首页
/ Flutter 框架 Tracing 测试机制:dev/tracing_tests 如何验证 Timeline 事件与构建期 Tree-Shaking

Flutter 框架 Tracing 测试机制:dev/tracing_tests 如何验证 Timeline 事件与构建期 Tree-Shaking

2026-09-06 10:18:25作者:谭伦延

本文以 dev/tracing_tests/README.md 为核心,讲解 Flutter 框架 tracing(时间线追踪)体系的两类测试机制:一是通过"影子应用"源码验证 tracing 相关逻辑在 profile/release 构建中被正确 tree-shake(编译剥离),二是通过 VM Service 连接验证框架各帧阶段(BUILD、LAYOUT、PAINT 等)确实向 Timeline 写入了追踪事件。读完本文,你能理解 --enable-vmservice 标志为何是这类测试的硬性前提、CI 中 runTracingTests 的完整校验流程,以及如何在本地复现整套验证。

一、目录定位:两类角色分明的 Tracing 测试

dev/tracing_tests 是 Flutter 仓库中专门验证 tracing 逻辑的独立包,其 pubspec.yaml 声明了 flutter SDK 依赖和 vm_service 包(后者是连接 Dart VM Service 读取时间线数据的关键)。README 将该目录的内容明确划分为两部分,两者用途完全不同:

  1. "Application"(影子应用)lib/test.dartlib/control.dart 两个文件供 CI 脚本 runTracingTests 使用,用来检查框架中的 tracing 逻辑在 profile 和 release 构建下是否被编译剥离(compiled out)。README 特别强调:这两个文件不打算被直接运行,文件中的特定字符串会被 CI 脚本逐字搜索(searched for verbatim)。
  2. Tests(时间线测试)test/ 目录下的测试验证 trace 数据确实被写入 timeline。由于它们通过连接 VM Service 来读取时间线,必须使用 flutter test --enable-vmservice 运行——缺少该标志时测试会直接失败。

下面分别深入这两部分。

二、"影子应用":用字符串指纹验证 tracing 的编译剥离

2.1 两个入口文件的设计意图

lib/test.dart 定义了一个最小的渲染树:TestWidgetLeafRenderObjectWidget)+ RenderTestRenderBox),其核心代码有三处"指纹":

class TestWidget extends LeafRenderObjectWidget {
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    // This string is searched for verbatim by dev/bots/test.dart:
    properties.add(MessageProperty('test', 'TestWidget.debugFillProperties called'));
  }
}

class RenderTest extends RenderBox {
  @override
  void performResize() {
    Timeline.instantSync('RenderTest.performResize called');
    size = constraints.biggest;
  }
  // ...
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(MessageProperty('test', 'RenderTest.debugFillProperties called'));
  }
}

Future<void> main() async {
  if (kDebugMode)     { print('BUILT IN DEBUG MODE'); }
  if (kProfileMode)   { print('BUILT IN PROFILE MODE'); }
  if (kReleaseMode)   { print('BUILT IN RELEASE MODE'); }

  // The point of this file is to make sure that toTimelineArguments is not
  // called when we have debugProfileBuildsEnabled (et al) turned on. If that
  // method is not called then the debugFillProperties methods above should also
  // not get called and we should end up tree-shaking the entire Diagnostics
  // logic out of the app.
  debugProfileBuildsEnabled = true;
  debugProfileLayoutsEnabled = true;
  debugProfilePaintsEnabled = true;
  runApp(const TestWidget());
}

注意源码注释点明了测试目标:即使运行时打开了 debugProfileBuildsEnabled 等开关,只要 toTimelineArguments 在 profile 构建中没有被真正调用,Dart AOT 编译的 tree shaker 就应该把整个 Diagnostics 相关逻辑(包括 debugFillProperties 里的字符串)从最终产物中剥离。而 BUILT IN DEBUG/PROFILE/RELEASE MODE 三条 print 则作为"构建模式正确性"的对照指纹。

lib/control.dart 是极简的对照组(control),只有两行有效代码:

DiagnosticsNode.message('TIMELINE ARGUMENTS TEST CONTROL FILE').toTimelineArguments();

它的作用是排除"整个文件没被编译进去"这种假阳性:如果 profile 构建中连这个直接调用 toTimelineArguments 的对照字符串都找不到,说明不是 tree-shaking 生效,而是构建/文件本身出了问题。

2.2 CI 侧的校验流程:runTracingTests

README 提到这些文件被 dev/bots/test.dartrunTracingTests 使用;在当前仓库中,该函数实际位于 dev/bots/suite_runners/run_framework_tests.dart(作为 runSlow 慢测试套件的一部分执行,见 runSlow)。其 verifyTracingAppBuild 的工作流程是:

  1. dev/tracing_tests 下执行 flutter build appbundle --profile|--release lib/<sourceFile>,产出 build/app/outputs/bundle/<mode>/app-<mode>.aab
  2. 解码 AAB(Android App Bundle)zip 包,取出 base/lib/arm64-v8a/libapp.so 的字节内容;
  3. 将其按 UTF-8 宽容解码为字符串集合,对"期望存在(allowed)"和"期望缺席(disallowed)"两组字符串指纹逐一检查,任何一项不符即 foundError 失败;
  4. 检查完执行 flutter clean 后继续下一组构建。

三组指纹校验矩阵如下(均引自 runTracingTests):

构建 入口文件 必须出现(allowed) 必须缺席(disallowed)
--profile control.dart TIMELINE ARGUMENTS TEST CONTROL FILEtoTimelineArguments used in non-debug build BUILT IN DEBUG MODEBUILT IN RELEASE MODE
--profile test.dart BUILT IN PROFILE MODERenderTest.performResize calledBUILDLAYOUTPAINT BUILT IN DEBUG/RELEASE MODE、两条 debugFillProperties called 字符串、toTimelineArguments used in non-debug build
--release test.dart BUILT IN RELEASE MODERenderTest.performResize called BUILT IN DEBUG/PROFILE MODEBUILDLAYOUTPAINT、两条 debugFillProperties called 字符串、toTimelineArguments used in non-debug build

这张矩阵体现了分层结论:

  • profile 构建:框架级的 BUILD/LAYOUT/PAINT 阶段追踪应保留(它们由 Timeline.startSync 在 profile 模式下输出,源码注释还说明 LAYOUTPAINT 也因 RenderObject.toStringShort 中的 NEEDS-LAYOUT/NEEDS-PAINT 而存在),但 Diagnostics 增强参数路径(toTimelineArguments)必须被剥离;
  • release 构建:连框架的阶段追踪事件都不应出现,只保留最小运行逻辑;
  • 对照组中 toTimelineArguments used in non-debug build 这条字符串的语义值得注意——它来自框架源码 packages/flutter/lib/src/foundation/diagnostics.dart 中的断言消息:toTimelineArguments 仅在 debug 构建下合法调用,一旦在非 debug 构建中被执行就会抛出带该字符串的异常。profile 对照组里它"必须出现",恰恰证明函数体被完整编译进了产物(字符串是产物的一部分);而真实测试 test.dart 里它"必须缺席",证明没有任何调用点残留、函数被 tree shaker 整体丢弃。

另外,debugProfileBuildsEnabled 等开关是框架中的普通运行时布尔变量,定义于 packages/flutter/lib/src/widgets/debug.dartbool debugProfileBuildsEnabled = false;),这解释了为什么 test.dart 能在 profile 构建中"运行时打开"这些开关而不影响编译期的剥离判断——两者正交。

三、test/ 目录:连接 VM Service 验证 Timeline 写入

3.1 为什么必须带 --enable-vmservice

README 的核心结论是:这里的测试"测试 trace 数据被写入 timeline 的方式就是连接 VM Service",因此必须 flutter test --enable-vmservice。这在 test/common.dart 中有直接印证——initTimelineTestssetUpAll 通过 dart:developerService.getInfo() 获取 VM Service 地址,若 serverUri 为 null(即未开 vmservice)则立即失败:

void initTimelineTests() {
  setUpAll(() async {
    final developer.ServiceProtocolInfo info = await developer.Service.getInfo();
    if (info.serverUri == null) {
      fail('This test _must_ be run with --enable-vmservice.');
    }
    _vmService = await vmServiceConnectUri(
      'ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws',
    );
    await _vmService.setVMTimelineFlags(<String>['Dart']);
    isolateId = developer.Service.getIsolateId(isolate.Isolate.current)!;
  });
}

工具链要点:

  • setVMTimelineFlags(<String>['Dart']):向 VM 声明要采集 Dart 时间线轨道,否则读不到事件;
  • fetchTimelineEvents()getVMTimeline() 拉取事件后立刻 clearVMTimeline(),保证每帧断言读取的是"增量窗口";
  • fetchInterestingEvents 只保留 ph == 'B'(Chrome tracing 格式的 Begin 标记,而非 E 结束标记)且名称在感兴趣集合内的事件,从而把一对 Begin/End 事件折叠为单一名称序列;
  • runFrame(callback) 借助 SchedulerBinding.instance.endOfFrame 调度一帧并等待完成,让断言精确绑定到"某一次帧";
  • ZoneIgnoringTestBinding 是历史遗留的 binding(注释明确新测试应避免依赖它),用于兼容未正确校验 Zone 的旧测试写法。

3.2 一帧的标准追踪事件序列

test/timeline_test.dart 是其中最具代表性的用例。它构造 TestRoot 状态树,逐次开启不同追踪开关并断言每帧产生的事件名序列。核心事实是:无论开启 debugProfileBuildsEnableddebugProfileLayoutsEnabled 还是 debugProfilePaintsEnabled,只要重建同一棵树,一帧都会完整产出如下六个阶段事件

BUILD → LAYOUT → UPDATING COMPOSITING BITS → PAINT → COMPOSITING → FINALIZE TREE

这正是 Flutter 帧管线(Build/Layout/Paint/Compositing)在 Timeline 中的投影。测试还验证了"增强时间线参数"能力:

  • 开启 debugProfileBuildsEnabled + debugEnhanceBuildTimelineArguments 后,BUILD 阶段内会出现以 $ 前缀命名的细粒度事件(如 $Placeholder),其 args 携带 debugFillProperties 输出——例如断言 args['color'] 等于 const Color(0xffffffff) 的字符串形式;
  • debugProfileLayoutsEnabled + debugEnhanceLayoutTimelineArguments 会产出 $RenderCustomPaint 事件,args['creator'] 指向创建它的 widget(以 CustomPaint 开头、包含 Placeholder),args['painter'] 形如 _PlaceholderPainter#...;paints 阶段同理。

这条链路与第二节的"影子应用"正好互为镜像:timeline_test 在 debug 测试运行时验证增强参数写入 Timeline,而 runTracingTests 在 profile 产物字节中验证同一套 Diagnostics 逻辑被剥掉了。测试末尾的 skip: isBrowser 注释说明其依赖 dart:isolateio,仅在非浏览器环境执行。

3.3 其他专项用例

test/ 目录下还有若干聚焦特定子系统追踪的测试文件,均复用 common.dart 的 VM Service 基建:

四、本地运行方式与适用前提

结合 README 与上述源码,运行方式可以归纳为:

# 在仓库根目录下,进入 dev/tracing_tests 后运行时间线测试
cd dev/tracing_tests
flutter test --enable-vmservice

注意事项:

  • 缺少 --enable-vmservice 必然失败common.dartsetUpAll 中显式 fail,这不是偶发问题而是设计约束;
  • 不要在本地把 lib/test.dartlib/control.dart 当成应用去 flutter run:README 明确它们是 CI 校验用的"素材",单独运行没有意义,其价值体现在 flutter build appbundle 产物中的字符串指纹里;
  • 完整复现 CI 的 tree-shaking 校验需要在 dev/tracing_tests 下执行 flutter build appbundle --profile/--release lib/<file>,并具备 Android 构建工具链以产出 .aab
  • 该包声明了 resolution: workspace,属于 monorepo 工作区解析,需在仓库根环境(含 Dart SDK ^3.11.0-0,见 pubspec.yaml)中解析依赖。

五、小结

dev/tracing_tests 用两条相互独立又互相印证的路线守护 Flutter 的 tracing 逻辑:

  1. 编译期路线lib/test.dart + lib/control.dart + runTracingTests):以 profile/release 的 AOT 产物(libapp.so)为被检对象,用字符串指纹矩阵证明"追踪事件保留、Diagnostics 增强逻辑剥离、构建模式正确"三件事同时成立;
  2. 运行时路线test/ 目录 + VM Service):以 --enable-vmservice 为前提连接 VM,逐帧断言 BUILD/LAYOUT/UPDATING COMPOSITING BITS/PAINT/COMPOSITING/FINALIZE TREE 事件序列与增强参数(creatorpaintercolor 等)正确写入 Timeline。

对使用者而言,这也解释了日常性能分析工具(读取 Timeline 的 Profiler 类工具)背后的数据来源:框架在 profile 构建中通过 Timeline API 写入的阶段事件,正是这套测试逐条锁定的行为契约。

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

项目优选

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