一、案例中的现象

   某个新机型性能摸底时,发现安装相同app版本情况下,新机型从trace查看帧率低很多。通过抓取trace分析发现,主要卡在app进程的RenderThread线程的dequeueBuffer阶段,trace如下如所示:
卡在app进程的RenderThread线程的dequeueBuffer阶段trace截图计算帧率:45.94fps,这里计算时抛掉前面明显的在案件按键之后layout耗时,因为该问题原因已知。计算帧率:45.94fps

二、分析思路

   首先dequeueBuffer是从sf的BufferQueue中获取一个可用的buffer,该函数耗时了说明是获取buffer过程卡住了

2.1 app-sf的生产者-消费者模型中,buffer的流向

注:曲线代表跨进程,实现代表进程内访问
buffer的流向示意图

2.2 按照这个模型,什么情况下会导致dequeueBuffer卡住?

有了上述的buffer流向,其实不难看出:
当三个buffer都queuBuffer到了BufferQueue中,但是sf侧没有及时合成完毕重新放入到BufferQueue中,那么自然就无法及时拿到buffer了。

2.3 再看sf的合成过程是否卡顿?

在这里插入图片描述

从上述trace截图分析,sf合成逻辑并没有卡顿,所以不难看出丢帧的原因是sf消费buffer时机延后导致的丢帧。

2.4 sf合成没卡,那为什么sf会延后消费buffer?

这个问题涉及到一个RenderThread提交绘制指令给GPU的时机问题。
RenderThread工作的几个阶段:

  • 1、syncFrameState
  • 2、dequeueBuffer
  • 3、eglSwapBuffers: “把画好的画交给显示系统”
eglSwapBuffers(display, surface)
└── eglSwapBuffersWithDamageKHR()  (优化版,只更新脏区域)
    │
    ├── [GL路径] 隐式 glFlush(如未flush)
    │
    ├── ① EGL Layer 处理
    │   └── 通知 EGLSurface 准备交换
    │
    ├── ② ANativeWindow::queueBuffer()
    │   └── 将渲染完成的 GraphicBuffer
    │       放回 BufferQueue
    │
    ├── ③ SurfaceFlinger 唤醒
    │   └── BufferQueue 通知 SF 有新帧
    │
    └── ④ dequeueBuffer(下一帧准备)
        └── 从 BufferQueue 取下一个空闲buffer
  • 4、flush commands: “告诉GPU开始干活”,注意这里只是把准备好的指令提交,并不代表GPU的pipeline一定立即开始,何时开始由GPU自己的command调度决定。
    面向 渲染后端(Skia/GL/Vulkan)
    操作对象:CommandBuffer / RenderPass
flush commands
└── SkiaOpenGLPipeline::draw() 或 SkiaVulkanPipeline::draw()
   └── skgpu::ganesh::FlushAndSubmit()
       ├── GrDirectContext::flush(flushInfo)
       │   ├── ① OpsTasks 排序与合并
       │   ├── ② RenderPass 构建
       │   ├── ③ 资源上传
       │   └── ④ 生成底层API调用
       │
       └── GrDirectContext::submit(syncCpu=false)
           ├── GL路径:  glFlush()
           └── VK路径:  vkQueueSubmit()
  • 5、queueBuffer
    RenderThread工作的几个阶段
2.5 如何关联上GPU xxx的waiting for completion xxx轨道信息

这个问题,做为一个UI崽来说,属实是知识盲区了,确实不懂,求问ChatGpt,把截图和问题描述给他,回答如下:
trace 中 waiting for GPU completion

eglSwapBuffersWithDamageKHR是GPU执行pipeline被真正提交(submit)的时机,那么其内部操作逻辑时序如下:
1 提交 GPU command buffer,这个只是把命令提交给了GPU队列,是异步的并非同步的立即执行
2 触发 buffer swap
3 queuBuffer

那么按照如上的架构设计,就有一个问题,通过queueBuffer提交给BufferQueue的buffer可能还未绘制完成,这时候即使vsync-sf到来,sf也是无法立即acqureBuffer进行合成的

那么,sf就需要等GPU绘制完成,才能进行合成逻辑,由此产生了App进程的GPU xxx的轨道上的waiting for completion xx(waitforever)的trace。

sf的fence机制:

Android 图形管线原理: 在 Android 中,App 提交(Queue)Buffer 给 SF 时,通常会携带一个 Acquire Fence。这个 Fence 代表了 GPU 对该 Buffer 的渲染进度。当 VSYNC-sf 到达时,SF 必须检查这个 Fence 是否已经 signal(即 GPU 是否已经真正把画面画完了)。
在这里插入图片描述

三、总结分析结论

这个案例的关键点不在主线程,也不在 sf 合成逻辑本身,而在 GPU 绘制完成时间延后。

完整链路可以概括为:

  1. App 侧 RenderThread 在 eglSwapBuffersWithDamageKHR 附近提交 GPU command buffer,并通过 queueBuffer 把 buffer 提交给 BufferQueue。
  2. buffer 被提交给 BufferQueue,并不代表 GPU 已经完成绘制,SurfaceFlinger 还需要等待对应的 Acquire Fence signal。
  3. 如果 GPU 绘制完成时间延后,即使 vsync-sf 到来,sf 也无法立即消费这个 buffer 参与合成。
  4. sf 消费 buffer 的时机延后后,BufferQueue 中可复用的空闲 buffer 释放不及时,App 后续 dequeueBuffer 就可能被卡住。
  5. 因此,trace 上看到的 RenderThread dequeueBuffer 耗时,本质上是 GPU 绘制完成时间延后在 App 侧表现出来的连锁结果。

所以,本案例可以归因于卡 GPU 导致的丢帧。分析类似问题时,不能只看 App 侧 dequeueBuffer 耗时,也不能只盯 BufferQueue 是否有空闲 buffer,而要继续确认 buffer 为什么没有及时回到可用状态:是 sf 合成阶段本身慢了,还是 acquire fence 一直没有 signal,导致 sf 无法及时消费 App 提交的 buffer。

这个分析方法不只适用于 dequeueBuffer 卡顿。遇到 queueBuffer 异常耗时时,也可以沿着 BufferQueue 状态、消费者消费节奏和 fence signal 时机继续排查。只是两者处在 buffer 流转链路的不同阶段:dequeueBuffer 更关注 App 为什么拿不到空闲 buffer,queueBuffer 更关注 App 提交 buffer 的过程为什么被阻塞或变慢,不能直接把两者的原因等同。

参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐