阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

Flutter 卡顿问题的监控与思考

  • 2020-06-30
  • 本文字数:4864 字

    阅读完需:约 16 分钟

Flutter卡顿问题的监控与思考

背景

使用 Flutter 技术构建的应用,一直以高性能高流畅度著称。但是随着应用复杂度越来越高,Flutter 会出现一些页面流畅度明显低于 Native 的情况,甚至可能发生一些卡顿。而很多时候卡顿都发生在线上,即使获得了用户的操作路径,也难以重现。如果我们有一套卡顿监控系统,能够帮助我们捕获到卡顿时的堆栈,那么在发生卡顿的时候,我们就可以定位到具体是哪个函数引起的卡顿,从而解决这些问题。


既然想要设计一个卡顿监控系统,那么我们就需要先解决两个问题:


  • 如何判断当前发生了卡顿。

  • 如何在卡顿时获取堆栈。

如何判断卡顿

既然我们希望能够抓取 Flutter 的卡顿堆栈,那么首先我们得先有办法判断 Flutter App 是否发生卡顿。为此,我们先来简单回顾一下 Flutter 的渲染原理。Flutter 的 UI Task Runner 负责执行 Dart 代码,而 Flutter 的渲染管线也是在 UI Task Runner 中运行的。每次 Flutter App 的界面需要更新时,Framework 会通过 ui.window.scheduleFrame 通知 Engine。然后 Engine 会注册一个 Vsync 信号的回调,在下一个 VSync 信号到来之际,Engine 会通过 ui.window.onBeginFrame 和 ui.window.onDrawFrame 回调给 Framework 来驱动 Flutter 渲染管线,渲染管线中的 Build、Layout、Paint 一一被执行,生成了最新的 Layer Tree。最后 Layer Tree 通过 ui.window.render 发送到了 Engine 端,交给 GPU Task Runner 做光栅化与上屏。



我们可以定义一个卡顿阈值,在 ui.window.onBeginFrame 开始计时,在 ui.window.onDrawFrame 做好卡口,如果渲染管线的执行时间过长,大于卡顿阈值,那么我们就可以判断发生了卡顿。


如果等到我们判断出了当前发生了卡顿,再去采集堆栈,为时已晚。因此,我们需要另外起一个 Isolate,每隔一小段时间就去采集一次 root Isolate 的堆栈,那么当我们判断出现卡顿时,只要将从卡顿开始到卡顿结束的这段时间采集到的堆栈做一次聚合,就能知道是哪个方法引起了卡顿了。


举个例子,如果我们定义的卡顿阈值为 100ms,然后每隔 5ms 采集一次卡顿堆栈,假设 ui.window.onBeginFrame 开始到 ui.window.onDrawFrame 结束总共耗时 200ms,其中 foo 方法耗时 160ms,bar 方法耗时 30ms,其余方法耗时 10ms。那么在这段时间,我们一共能采集到 40 个堆栈,其中有 32 个堆栈的栈顶为 foo 方法,6 个堆栈的栈顶为 bar 方法。在堆栈聚合后,我们就能发现 foo 方法的耗时大约为 160ms,而 bar 方法的耗时大约为 30ms。


这个方案看上去比较简单,整体思路上也是借鉴了 Android 端的卡顿检测框架 BlockCanary,那么这个方案是否可行呢?我们需要解决的第一个问题,就是如何在另一个 Isolate 去采集 root Isolate 的堆栈。

堆栈采集方案一:修改 Dart SDK

在 Dart 中,我们可以通过调用 StackTrace.current 来获取当前 Isolate 的调用栈。那么它能否帮助我们获取卡顿时候的堆栈呢?非常可惜,它做不到这一点。


举个例子:假设我们有一个名叫 foo 的方法,耗时大概 300ms,在 root Isolate 中执行,很明显,这个方法会引起卡顿。而 StackTrace.current 并不能获取帮助我们定位到这个耗时的 foo 方法。我们需要的,是在另一个 Isolate 中采集到 root Isolate 堆栈的方法。

官方的相关 issue

并不是只有我们有这个诉求,google 的同学在 flutter 的 repo 下,提了 Issue:Add API to query main Isolate’s stack trace #37204。这个 Issue 的大致内容是说,希望 Dart 能够提供一个 API,用于在另一个 Isolate 去采集 main Isolate 堆栈,当前这个 Issue 还是 open 的状态。


Issue 提出到现在大概已经过去一年时间了,为什么这个 API 还是没有实现呢?其实,实现这个 API 本身并不困难,只是官方有一些自己的考量,其中之一就是这可能会引入安全性问题:Dart Isolate 之间本应该相互隔离,如果添加了这个 API,那么可能会有黑客通过多次调用该 API 来获取大量的堆栈信息,再通过比对这些堆栈的差异来对加密秘钥发起定时攻击等。看来官方短期之内是不会提供这个 API 了,那么我们是不是可以先试试通过修改 Dart SDK 来实现类似的功能。

通过修改 SDK 获取 API

我们先来看看 StackTrace.current 是如何获取堆栈的吧



我们可以看到,StackTrace.current 方法的修饰符中有一个 external,这代表了这是一个 external 函数,Dart 中的 external 函数意味着这个函数的声明和实现是分开的,这里只是声明,实现在另一个地方,其实现的地方如下:



从 StackTrace.current 的实现中有一个 native 关键字,native 关键字是 Dart 的 Native Extension 的关键字,意味着这个方法是 C/C++实现的。Native Extension 与 Java 中的 JNI 非常的相似。



我们终于找到了实现,CurrentStackTrace,通过观察发现,它的第一个参数是一个 thread。可见 CurrentStackTrace 方法获取的堆栈是基于 thread 的,那么是不是说,如果我们在另一个 Isolate 中,将 root Isolate 对应的 Thread 作为参数,传入到 CurrentStackTrace 方法里,就能获得 root Isolate 对应的堆栈了呢?


为了验证我们这个想法,我们新增了两个方法:StackTrace.prepare 和 StackTrace.root,我们在 root Isolate 中调用 StackTrace.prepare,将 root Isolate 的 thread 对象使用静态变量 rootIsolateThread 保存起来。StackTrace.prepare 对应的 C++实现如下



然后我们新开一个 Isolate,在这个新的 Isolate 中,我们调用 StackTrace.root 来获取 root Isolate 的堆栈,StackTrace.root 对应的 C++实现如下



经过验证发现,通过这个方案,的确能在另一个 Isolate 中获取 root Isolate 的堆栈。当然上面的修改主要还是为了验证可行性,如果真的要采用修改 Dart SDK 的方案,还有非常多的地方需要考虑。


修改 Dart SDK 的这个方案大大增加了后期的维护成本,有没有可能存在一种不修改 Dart SDK,还是能获取到堆栈的方案呢?

堆栈采集方案二:AOT 模式下采集堆栈(暂停线程)

在不修改 Dart SDK 的前提下获取堆栈,听上去感觉是一个不可能完成的任务。但是有时候我们遇到了问题,或许转变一下思路,就能找到答案。

AOT 模式与符号表

让我们一起来梳理一下我们的诉求,首先我们设计的是一个线上卡顿监控方案,这个场景下的 Dart 代码是基于 AOT 编译的,在 iOS 端其产物为 App.framework,在 Android 端则为 libapp.so。基于 AOT,也就意味着 Dart 代码(包括 SDK 和你自己的)会被编译成平台相关的机器码。


那么 Dart 语言 AOT 编译生成的可执行程序与 C 语言编译生成的可执行程序,是否有区别呢?从操作系统的角度来看,它们并没有什么本质区别。操作系统只关心这个可执行程序如何加载,如何执行,至于程序是从 C 语言还是 Dart 语言编译过来的,它并不关心。


我们先来把目光聚焦到 Dart 代码在 iOS 端 profile 模式下的产物 App.framework。从 iOS 的视角触发,这是一个 Embedded Framework。我们可以使用 nm 命令导出其符号表,以下是符号表的一部分:



我们惊喜地发现,这些符号与 Dart 函数几乎是一一对应。比如符号 PrecompiledElementupdate_260,很明显对应的 Dart 函数为 Element.update。


有了这份符号表,也就意味着,如果我们能采集到 root Isolate 对应线程的 native 的堆栈,我们就可以通过符号化来还原出当时 Dart 函数的调用栈。而且我们也不再需要去寻找从另一个 Isolate 获取 root Isolate 的 Dart 堆栈的方法了。与之对应的,我们只需要能够在另一个线程获取 root Isolate 对应的线程的 native 堆栈即可。

堆栈采集的方案

栈帧采集的方案整体思路如下:


  1. 获取当前进程中的所有线程,并找到 Flutter UI TaskRunner 对应的线程

  2. 暂停该线程,并获取该线程当前的寄存器的值,重点为 PC 和 FP

  3. 根据栈帧回溯算法,获取堆栈

  4. 让该线程继续运行

  5. 在当前进程或者远端做符号化

方案实现

接下来我们来看看如何实现这个方案,我们以 iOS 端为例子,来说明如何实现这个方案:


在 iOS 端,我们可以通过 API task_threads来获取所有的线程,代码如下:



我们可以通过比对线程名字来定位到 UI Task Runner 对应的线程,如果是 Flutter 单 Engine 方案,那么 UI Task Runner 对应的 Thread 的名字应为"io.flutter.1.ui"。



在采集堆栈前,我们得先暂停这个线程。



暂停线程后,我们就可以通过 thread_get_state去获取这个线程此时此刻的寄存器的值了,其中能够帮助我们做栈帧回溯的两个寄存器分别是 pc 和 fp,我们这里的代码是以 arm64 为例子的,在实际的产品中,还需要考虑到其他的架构:



获取 pc 和 fp 后,就可以进行栈帧回溯了。至于如何进行栈帧回溯,我们会在下一个小节单独说明。栈帧采集完之后,我们需要让线程继续运行:



以上就是 iOS 端堆栈采集方案的大体实现了。Android 端想实现这个方案,思路上大同小异,无论是找到所有的线程,定位到 UI Task Runner 对应的线程,还是线程的暂停和恢复,都能找到解决方案。唯一比较麻烦的地方在于如何获取另一个线程暂停时的寄存器的值,这部分可以使用 ptrace 来完成,不过这个需要起一个独立的进程。

栈帧回溯原理

上文说到,我们获得了 pc 和 fp 寄存器的值,该如何做栈帧回溯呢?

这里我们以ARM64栈帧布局为例子(也就是上图)。每次函数调用,都会在调用栈上,维护一个独立的栈帧,每个栈帧中都有一个FP(Frame Pointer),指向上一个栈帧的FP,而与FP相邻的LR(Link Register)中保存的是函数的返回地址。也就是我们可以根据FP找到上一个FP,而与FP相邻的LR对应的函数就是该栈帧对应的函数。回溯的算法如下

堆栈采集完毕后,我们只需要将采集到的堆栈进行符号化即可。


堆栈采集方案三:AOT 模式下采集堆栈(通过信号)

性能瓶颈

上面的这个方案可能会对性能造成一些影响,堆栈回溯本身并不耗时,真正的耗时在于线程的暂停和恢复。线程暂停后,线程就会进入阻塞状态,而去恢复线程时,线程并不会立即执行,而是会进入就绪状态,等待内核调度为其分配 CPU 时间片。所以在这个方案,每一次采集线程堆栈,都意味着这个线程的状态可能会从运行态到阻塞态再到就绪态。



那么有没有更为轻量级的采集堆栈的方案?

信号机制原理

信号(Signal)是事件发生时对进程的通知机制,有时候也称之为软件中断。一般发给进程的信号,通常是由内核产生的,比如访问了非法的内存地址,或者被 0 除等等,当然,一个进程可以向另一个进程或者自身发送信号。如果进程注册了信号处理器(Signal Handler),那么当收到信号后,就会中断当前程序,开始调用信号处理器程序,等信号处理器程序执行完成后,当前程序会在被中断的位置继续执行。


新方案的实现

我们先注册一个信号处理器,用于采集堆栈。接着,我们还是启动一个采集线程,每隔一段时间,向 UI Task Runner 发送一个信号。当收到信号后,UI Task Runner 对应的线程就会被中断,执行信号处理器程序来采集堆栈,堆栈采集完后,程序会从中断点回复执行。


我们来看看这个方案具体如何实现,这次我们以 Android 端为例子:


首先我们先注册一些 signal handler,用于在收到信号时采集堆栈



接着我们每隔一段时间,就向 UI Task Runner 对应的线程发送一个信号。



信号到达后,该线程就会中断当前执行的程序,然后调用 signal handler 采集堆栈,其中 signalHandler 的实现如下



实际上,FaceBook 的性能监控方案 profilo,以及 Dart VM 的 CPU Profiler,均使用了这个方案来采集堆栈。

堆栈采集方案对比

我们来对比一下上面提到的 3 个方案,它们的区别如下图所示:



我们可以看到,方案三无需修改 SDK,所以维护成本较低,并且在三个方案中它的性能损耗是最低的。最终我们决定采用方案三来作为我们堆栈采集的方案。

总结

本文主要介绍了我们在设计 Flutter 卡顿监控系统的一些思路,给出了如何判断卡顿跟如何获取堆栈的思考和探索,目前这个方案的产品化正在进行当中。Flutter 作为高性能的跨平台方案,其渲染性能从理论上来说,可以做到不弱于原生。同时 Flutter 在性能体验方向上,和原生相比,还有非常多值得探索的地方,让我们一起不忘初心,继续朝着这个方向前进。


本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)。


原文链接


https://mp.weixin.qq.com/s/-BTEkHYeh_tHqJY2UNI_xw


2020-06-30 10:003396

评论

发布
暂无评论
发现更多内容

用友全球财务数智化解决方案助力企业对标世界一流财务体系,护航中企出海

用友BIP

智能财务 中企出海

开启中文智能之旅:探秘超乎想象的 Llama2-Chinese 大模型世界

汀丶人工智能

人工智能 自然语言处理 llama 大语言模型 llama2

发行版兴趣小组季度动态:Anolis OS 支持大热 AI 软件栈,引入社区合作安全修复流程

OpenAnolis小助手

AI 操作系统 CVE 龙蜥社区 发行版

ChatGPT 是如何产生心智的? | 京东云技术团队

京东科技开发者

人工智能 机器学习 ChatGPT 企业号10月PK榜

开发神器 - 亚马逊 CodeWhisperer 代码开发 AI 工具

亚马逊云科技 (Amazon Web Services)

对话在行人|荣庆物流:通过数智化转型使工作时效提升70%

用友BIP

2023全球商业创新大会 对话在行人

突破零基础:NineData新手任务完全指南

NineData

数据恢复 SQL开发 NineData 新手任务 数据源创建

EVE-NG:一种强大的网络模拟器和实验平台

小魏写代码

利用ChatGPT提升测试工作效率——测试工程师的新利器(一) | 京东云技术团队

京东科技开发者

人工智能 测试 企业号10月PK榜

中国水泥行业数字化采购:驱动产业链供应链现代化的关键

用友BIP

数智采购 水泥行业

慢SQL治理经验总结

阿里技术

sql 慢SQL

OP链DAPP质押挖矿系统开发源码搭建

l8l259l3365

如何导出带有材质的GLB模型?

3D建模设计

glb 材质 纹理 贴图

用友深度参编!《煤炭行业信息技术应用创新发展报告(2023)》重磅发布

用友BIP

信创

关于征集中国人工智能产业发展联盟“人工智能基础平台(AI Infra)工作组”首批成员单位的通知

中国信通院AI Infra工作组

LAS Spark 在 TPC-DS 的优化揭秘

字节跳动数据平台

数据库 大数据 数据安全 数据研发 企业号10月PK榜

SRE实战:如何低成本推进风险治理?稳定性与架构优化的3个策略

TakinTalks稳定性社区

软件开发人员 Kubernetes 入门指南|Part 1

SEAL安全

Kubernetes 运维 软件开发 企业号10月PK榜

离职原因千万不要这样说!

王磊

Java

关于征集人工智能一体机系列标准参编单位的通知

中国信通院AI Infra工作组

侧发光透明LED显示屏特点优势

Dylan

商业 类型 LED显示屏 户外LED显示屏

基于Effect的组件设计 | 京东云技术团队

京东科技开发者

前端 React Hooks 企业号10月PK榜 effect

重复文件查找清理软件 Gemini 2激活版中文

mac大玩家j

Mac软件 重复文件清理工具 重复文件查找软件

私密离线聊天新体验!llama-gpt聊天机器人:极速、安全、搭载Llama 2

汀丶人工智能

人工智能 自然语言处理 nlp llama 大语言模型

OpenJDK17-JVM源码阅读-ZGC-并发标记 | 京东物流技术团队

京东科技开发者

ZGC 并发标记 企业号10月PK榜 JVM源码

Spring Boot 项目中 Bean 注入的方式介绍

Apifox

Java Spring Boot annotation bean Spring Boot bean

Flutter卡顿问题的监控与思考_文化 & 方法_皓黯_InfoQ精选文章