Systrace看GPU渲染花费时间之Fence

发布时间 2023-08-18 22:39:26作者: Hello-World3

一、前言

如上图所示的 Systrace 中,VSYNC-app 基本上没有什么变化,但是 VSYNC-sf 却一直在更新有可能是什么原因?

VSYNC-app 的作用通知 app 去开始进行绘制渲染更新 UI 了,DispSync 按照屏幕的刷新率的速率去通知 app,因此 app 会以跟屏幕刷新率匹配的速率去绘制渲染更新 UI。而在手游情况就有不同了,目前绝大部分手游都是使用游戏引擎,例如 Unity 或者 Unreal,而在这些游戏引擎中,引擎自己会去控制渲染的帧率(一般有多个帧率档位可以选择,例如和平精英有 20-90 的帧率可供选择),因此,手游就可以不去听 VSYNC-app 的速率,从而就有了上面 Systrace 中的现象了。

这期我们来一起学习一下如何在 Systrace 中查看 GPU 渲染花费的时间。建议大家在看这篇文章的时候可以配合着这篇 《fps 的计算原理》,缕清楚各个 fence 的作用和意义,有助于你理解这篇文章。


二、queueBuffer

我们应该经常能够看到一个 BufferQueue 中 Buffer 的流转图:

我们知道,app,也就是图中的 PRODUCER,它通过 dequeueBuffer() 从 BufferQueue 中获取到一个 Buffer,然后决定了要绘制的内容以后,会交给 GPU 去做最后的渲染工作,同时会通过 queueBuffer() 将 Buffer 还给 BufferQueue所以,在 PRODUCER 执行 queueBuffer() 的时候,渲染工作是没有完成的
那么我们能否从 Systrace 中了解到 GPU 何时渲染完成呢?如果 GPU 渲染的时间长,那么其实就可以初步判断出性能瓶颈出自于 GPU 侧了。


三、FenceMonitor

答案是可以的。在 Android Q 中,libgui 引入了一个新的内部类 —— FenceMonitor,作用是通过跟踪 Fence 的生命周期,在 Systrace 中展示一个 Fence 从产生到 signal 需要的时间。在 Surface.cpp 的 Surface::dequeueBuffer() 和 Surface::queueBuffer() 分别有两个 FenceMonitor 的静态变量,分别跟踪了 release fence 和 acquire fence 的生命周期:

int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {
    ......
        static FenceMonitor hwcReleaseThread("HWC release"); //传的是名字"HWC release"
        hwcReleaseThread.queueFence(fence);
        
int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {
    ......
        static FenceMonitor hwcReleaseThread("HWC release");
        hwcReleaseThread.queueFence(fence);

让我们来看一下它是如何实现这个效果的:


三、实现原理

class FenceMonitor {
public:
    explicit FenceMonitor(const char* name) : mName(name), mFencesQueued(0), mFencesSignaled(0) {
        std::thread thread(&FenceMonitor::loop, this);
        pthread_setname_np(thread.native_handle(), mName);
        thread.detach();
    }

    void queueFence(const sp<Fence>& fence) {
        char message[64];

        std::lock_guard<std::mutex> lock(mMutex);
        if (fence->getSignalTime() != Fence::SIGNAL_TIME_PENDING) {
            snprintf(message, sizeof(message), "%s fence %u has signaled", mName, mFencesQueued);
            ATRACE_NAME(message);
            // Need an increment on both to make the trace number correct.
            mFencesQueued++;
            mFencesSignaled++;
            return;
        }
        snprintf(message, sizeof(message), "Trace %s fence %u", mName, mFencesQueued);
        ATRACE_NAME(message);

        // 将需要跟踪声明周期的 fence 入队并且唤醒 threadLoop() 中阻塞的循环
        mQueue.push_back(fence);
        mCondition.notify_one();
        mFencesQueued++;
        ATRACE_INT(mName, int32_t(mQueue.size()));
    }

private:
    void loop() {
        while (true) {
            threadLoop();
        }
    }

    void threadLoop() {
        sp<Fence> fence;
        uint32_t fenceNum;
        {
            std::unique_lock<std::mutex> lock(mMutex);
            // 在没有执行 queueFence() 之前,mQueue 为空,会被阻塞在这里
            while (mQueue.empty()) {
                mCondition.wait(lock);
            }
            // 获取队头的 fence 开始进行声明周期的跟踪
            fence = mQueue[0];
            fenceNum = mFencesSignaled;
        }
        // 注意这里使用了一个块作用域的小技巧,下面会详细说明
        {
            char message[64];
            //这里的mName就是 New FenceMonitor 对象时指定的名字
            snprintf(message, sizeof(message), "waiting for %s %u", mName, fenceNum);
            ATRACE_NAME(message);

            status_t result = fence->waitForever(message); //也有trace打印
            if (result != OK) {
                ALOGE("Error waiting for fence: %d", result);
            }
        }
        {
            std::lock_guard<std::mutex> lock(mMutex);
            mQueue.pop_front();
            mFencesSignaled++;
            ATRACE_INT(mName, int32_t(mQueue.size()));
        }
    }

    const char* mName;
    uint32_t mFencesQueued;
    uint32_t mFencesSignaled;
    std::deque<sp<Fence>> mQueue;
    std::condition_variable mCondition;
    std::mutex mMutex;
};

构造函数会去创建一个线程并且执行 loop() 这个函数,而 loop() 就是一个死循环,会一直去运行 threadLoop()。在 threadLoop() 中,分为三个块作用域(作用域中定义的对象退出作用域后析构),首先 threadLoop() 会阻塞在第一个块作用域,直到执行 queueFence() 为止。

而当执行 queueFence() 以后,传入的 fence 会入队,并且唤醒 threadLoop() 中循环,然后获取到队头的 fence。

进入第二个块作用域,开始进行生命周期的监听。Fence 生命周期的监听使用了 C++ 块作用域和 ATRACE_NAME() 析构函数的小技巧。ATRACE_CALL() 实际上是 ATRACE_NAME() 的一个特例(传入的字符串参数为当前调用的函数名),而 ATRACE_NAME() 本身定义了一个类型为 ScopedTrace 的变量,其构造函数会去调用 atrace_begin(),析构函数会去调用 atrace_end()。因此,在第二个块作用域中,通过 ATRACE_NAME() 调用 ScopedTrace 的构造函数间接调用 atrace_begin(),然后调用 waitForever() 等待 fence 被 signal,接着第二个块作用域结束,调用 ScopedTrace 的析构函数间接调用 atrace_end(),从而达到了跟踪 fence 生命周期的效果。

我们就可以在 Systrace 中看到如下的内容:

每个 waiting for GPU completion xxx 的长度,就是 GPU 渲染所需要的时间(即 acquire fence 释放的总时间),通过这个时间,我们就可以来判断是否有 GPU bound 的现象。

相应的,waiting for HWC release XXX 的长度就是 release fence 的释放总时间在 release fence signal 之前,GPU 是无法对 dequeueBuffer() 拿到的 Buffer 去进行读写的(因为此时的 Buffer 所有者还处于 HWC),因此可以通过这一点来判断 Display 是否有问题。

 

参考:
https://juejin.cn/post/6879740386198323207