Flutter 如何判断卡顿,修改 Dart SDK

2020-07-0209:11:35APP与小程序开发Comments3,379 views字数 5040阅读模式

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

既然想要设计一个卡顿监控系统,那么我们就需要先解决两个问题:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

如何判断卡顿

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

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们可以定义一个卡顿阈值,在 开始计时,在 做好卡口,如果渲染管线的执行时间过长,大于卡顿阈值,那么我们就可以判断发生了卡顿。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

如果等到我们判断出了当前发生了卡顿,再去采集堆栈,为时已晚。因此,我们需要另外起一个 Isolate,每隔一小段时间就去采集一次 root Isolate 的堆栈,那么当我们判断出现卡顿时,只要将从卡顿开始到卡顿结束的这段时间采集到的堆栈做一次聚合,就能知道是哪个方法引起了卡顿了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

举个例子,如果我们定义的卡顿阈值为 100ms,然后每隔 5ms 采集一次卡顿堆栈,假设 开始到 结束总共耗时 200ms,其中 foo 方法耗时 160ms,bar 方法耗时 30ms,其余方法耗时 10ms。那么在这段时间,我们一共能采集到 40 个堆栈,其中有 32 个堆栈的栈顶为 foo 方法,6 个堆栈的栈顶为 bar 方法。在堆栈聚合后,我们就能发现 foo 方法的耗时大约为 160ms,而 bar 方法的耗时大约为 30ms。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

这个方案看上去比较简单,整体思路上也是借鉴了 Android 端的卡顿检测框架 BlockCanary,那么这个方案是否可行呢?我们需要解决的第一个问题,就是如何在另一个 Isolate 去采集 root Isolate 的堆栈。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

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

在 Dart 中,我们可以通过调用 来获取当前 Isolate 的调用栈。那么它能否帮助我们获取卡顿时候的堆栈呢?非常可惜,它做不到这一点。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

举个例子:假设我们有一个名叫 foo 的方法,耗时大概 300ms,在 root Isolate 中执行,很明显,这个方法会引起卡顿。而 并不能获取帮助我们定位到这个耗时的 foo 方法。我们需要的,是在另一个 Isolate 中采集到 root Isolate 堆栈的方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

官方的相关 issue

并不是只有我们有这个诉求,google 的同学在 flutter 的 repo 下,提了 Issue:Add API to query main Isolate’s stack trace #37204。这个 Issue 的大致内容是说,希望 Dart 能够提供一个 API,用于在另一个 Isolate 去采集 main Isolate 堆栈,当前这个 Issue 还是 open 的状态。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

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

通过修改 SDK 获取 API

我们先来看看 是如何获取堆栈的吧文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们可以看到, 方法的修饰符中有一个 external,这代表了这是一个 external 函数,Dart 中的 external 函数意味着这个函数的声明和实现是分开的,这里只是声明,实现在另一个地方,其实现的地方如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

从 的实现中有一个 native 关键字,native 关键字是 Dart 的 Native Extension 的关键字,意味着这个方法是 C/C++ 实现的。Native Extension 与 Java 中的 JNI 非常的相似。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们终于找到了实现,CurrentStackTrace,通过观察发现,它的第一个参数是一个 thread。可见 CurrentStackTrace 方法获取的堆栈是基于 thread 的,那么是不是说,如果我们在另一个 Isolate 中,将 root Isolate 对应的 Thread 作为参数,传入到 CurrentStackTrace 方法里,就能获得 root Isolate 对应的堆栈了呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

为了验证我们这个想法,我们新增了两个方法: 和 ,我们在 root Isolate 中调用 ,将 root Isolate 的 thread 对象使用静态变量 rootIsolateThread 保存起来。 对应的 C++ 实现如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

然后我们新开一个 Isolate,在这个新的 Isolate 中,我们调用 来获取 root Isolate 的堆栈, 对应的 C++ 实现如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

经过验证发现,通过这个方案,的确能在另一个 Isolate 中获取 root Isolate 的堆栈。当然上面的修改主要还是为了验证可行性,如果真的要采用修改 Dart SDK 的方案,还有非常多的地方需要考虑。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

修改 Dart SDK 的这个方案大大增加了后期的维护成本,有没有可能存在一种不修改 Dart SDK,还是能获取到堆栈的方案呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

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

在不修改 Dart SDK 的前提下获取堆栈,听上去感觉是一个不可能完成的任务。但是有时候我们遇到了问题,或许转变一下思路,就能找到答案。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

AOT 模式与符号表

让我们一起来梳理一下我们的诉求,首先我们设计的是一个线上卡顿监控方案,这个场景下的 Dart 代码是基于 AOT 编译的,在 iOS 端其产物为 ,在 Android 端则为 。基于 AOT,也就意味着 Dart 代码(包括 SDK 和你自己的)会被编译成平台相关的机器码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

那么 Dart 语言 AOT 编译生成的可执行程序与 C 语言编译生成的可执行程序,是否有区别呢?从操作系统的角度来看,它们并没有什么本质区别。操作系统只关心这个可执行程序如何加载,如何执行,至于程序是从 C 语言还是 Dart 语言编译过来的,它并不关心。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们先来把目光聚焦到 Dart 代码在 iOS 端 profile 模式下的产物 。从 iOS 的视角触发,这是一个 Embedded Framework。我们可以使用 nm 命令导出其符号表,以下是符号表的一部分:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们惊喜地发现,这些符号与 Dart 函数几乎是一一对应。比如符号 PrecompiledElementupdate_260,很明显对应的 Dart 函数为 。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

有了这份符号表,也就意味着,如果我们能采集到 root Isolate 对应线程的 native 的堆栈,我们就可以通过符号化来还原出当时 Dart 函数的调用栈。而且我们也不再需要去寻找从另一个 Isolate 获取 root Isolate 的 Dart 堆栈的方法了。与之对应的,我们只需要能够在另一个线程获取 root Isolate 对应的线程的 native 堆栈即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

堆栈采集的方案

栈帧采集的方案整体思路如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

  1. 获取当前进程中的所有线程,并找到 Flutter UI TaskRunner 对应的线程
  2. 暂停该线程,并获取该线程当前的寄存器的值,重点为 PC 和 FP
  3. 根据栈帧回溯算法,获取堆栈
  4. 让该线程继续运行
  5. 在当前进程或者远端做符号化

方案实现

接下来我们来看看如何实现这个方案,我们以 iOS 端为例子,来说明如何实现这个方案:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

在 iOS 端,我们可以通过 API task_threads来获取所有的线程,代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们可以通过比对线程名字来定位到 UI Task Runner 对应的线程,如果是 Flutter 单 Engine 方案,那么 UI Task Runner 对应的 Thread 的名字应为 ""。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

在采集堆栈前,我们得先暂停这个线程。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

暂停线程后,我们就可以通过 thread_get_state去获取这个线程此时此刻的寄存器的值了,其中能够帮助我们做栈帧回溯的两个寄存器分别是 pc 和 fp,我们这里的代码是以 arm64 为例子的,在实际的产品中,还需要考虑到其他的架构:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

获取 pc 和 fp 后,就可以进行栈帧回溯了。至于如何进行栈帧回溯,我们会在下一个小节单独说明。栈帧采集完之后,我们需要让线程继续运行:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

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

栈帧回溯原理

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

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

这里我们以 ARM64 栈帧布局为例子(也就是上图)。每次函数调用,都会在调用栈上,维护一个独立的栈帧,每个栈帧中都有一个 FP(Frame Pointer),指向上一个栈帧的 FP,而与 FP 相邻的 LR(Link Register)中保存的是函数的返回地址。也就是我们可以根据 FP 找到上一个 FP,而与 FP 相邻的 LR 对应的函数就是该栈帧对应的函数。回溯的算法如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

堆栈采集完毕后,我们只需要将采集到的堆栈进行符号化即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

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

性能瓶颈

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

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

那么有没有更为轻量级的采集堆栈的方案?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

信号机制原理

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

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

新方案的实现

我们先注册一个信号处理器,用于采集堆栈。接着,我们还是启动一个采集线程,每隔一段时间,向 UI Task Runner 发送一个信号。当收到信号后,UI Task Runner 对应的线程就会被中断,执行信号处理器程序来采集堆栈,堆栈采集完后,程序会从中断点回复执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们来看看这个方案具体如何实现,这次我们以 Android 端为例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

首先我们先注册一些 signal handler,用于在收到信号时采集堆栈文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

接着我们每隔一段时间,就向 UI Task Runner 对应的线程发送一个信号。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

信号到达后,该线程就会中断当前执行的程序,然后调用 signal handler 采集堆栈,其中 signalHandler 的实现如下文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

实际上,FaceBook 的性能监控方案 profilo,以及 Dart VM 的 CPU Profiler,均使用了这个方案来采集堆栈。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

堆栈采集方案对比

我们来对比一下上面提到的 3 个方案,它们的区别如下图所示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

Flutter 如何判断卡顿,修改 Dart SDK文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

我们可以看到,方案三无需修改 SDK,所以维护成本较低,并且在三个方案中它的性能损耗是最低的。最终我们决定采用方案三来作为我们堆栈采集的方案。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/19577.html

总结

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

  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/19577.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定