或許是迄今爲止第一篇講解 fps 計算原理的文章吧

前言

fps,是 frames per second 的簡稱,也就是我們常說的“幀率”。在遊戲領域中,fps 作爲衡量遊戲性能的基礎指標,對於遊戲開發和手機 vendor 廠商都是非常重要的數據,而計算遊戲的 fps 也成爲日常測試的基本需求。目前市面上有很多工具都能夠計算 fps,那麼這些工具計算 fps 的方法是什麼?原理是什麼呢?本文將針對這些問題,深入源碼進行分析,力求找到一個詳盡的答案(源碼分析基於 Android Q)

計算方法

目前絕大部分幀率統計軟件,在網上能找到的各種統計 fps 的腳本,使用的信息來源有兩種:一種是基於 dumpsys SurfaceFlinger --latency Layer-name(注意是 Layer 名字,不是包名,不是 Activity 名字,至於爲什麼,下面會解答);另一種是基於 dumpsys gfxinfo。其實這兩種深究到原理基本上是一致的,本篇文章專注於分析第一種,市面上大部分幀率統計軟件用的也是第一種,只不過部分軟件爲了避免被人反編譯看到,將這個計算邏輯封裝成 so 庫,增加反編譯的難度。然而經過驗證,這些軟件最後都是通過調用上面的命令來計算的 fps 的。

但是這個命令爲什麼能夠計算 fps 呢?先來看這個命令的輸出,以王者榮耀爲例(王者榮耀這種遊戲類的都是以 SurfaceView 作爲控件,因此其 Layer 名字都以 SurfaceView - 打頭):

> adb shell dumpsys SurfaceFlinger --latency "SurfaceView - com.tencent.tmgp.sgame/com.tencent.tmgp.sgame.SGameActivity#0"
16666666
59069638658663  59069678041684  59069654158298
59069653090955  59069695022100  59069670894236
59069671034444  59069711403455  59069687949861
59069688421840  59069728057361  59069704415121
59069705420850  59069744773350  59069720767830
59069719818975  59069761378975  59069737416007
59069736702673  59069778060955  59069754568663
59069753361528  59069794716007  59069770761632
59069768766371  59069811380486  59069787649600
......

輸出的這一堆數字究竟是什麼意思?首先,第一行的數字是當前的 VSYNC 間隔,單位是納秒。例如現在的屏幕是 60Hz 的,因此就是 16.6ms,然後下面的一堆數字,總共有 127 行(爲什麼是 127 行,下面也會說明),每一行有 3 個數字,每個數字都是時間戳,單位是納秒,具體的意義後文會說明。而在計算 fps 的時候,使用的是第二個時間戳。原因同樣會在後文進行解答。

fence 簡析

後面的原理分析涉及到 fence,但是 fence 囊括的內容衆多,因此這裏只是對 fence 做一個簡單地描述。如果大家感興趣,後面我會專門給 fence 寫一篇詳細的說明文章。

fence 是什麼

首先得先說明一下 App 繪製的內容是怎麼顯示到屏幕的:

App 需要顯示的內容要要繪製在 Buffer 裏,而這個 Buffer 是從 BufferQueue 通過 dequeueBuffer() 申請的。申請到 Buffer 以後,App 將內容填充到 Buffer 以後,需要通過 queueBuffer() 將 Buffer 還回去交給 SurfaceFlinger 去進行合成和顯示。然後,SurfaceFlinger 要開始合成的時候,需要調用 acquireBuffer() 從 BufferQueue 裏面拿一個 Buffer 去合成,合成完以後通過 releaseBuffer() 將 Buffer 還給 BufferQueue,如下圖:

BufferQueue

在上面的流程中,其實有一個問題,就是在 App 繪製完通過 queueBuffer() 將 Buffer 還回去的時候,此時僅僅只是 CPU 側的完成,GPU 側實際上並沒有真正完成。因此如果此時拿這個 Buffer 去進行合成/顯示的話,就會有問題(Buffer 可能還沒有完全地繪製完)。

事實上,由於 CPU 和 GPU 之前是異步的,因此我們在代碼裏面執行一系列的 OpenGL 函數調用的時候,看上去函數已經返回了,實際上,只是把這個命令放在本地的 command buffer 裏。具體什麼時候這條 GL command 被真正執行完畢 CPU 是不知道的,除非使用 glFinish() 等待這些命令完全執行完,但是這樣會帶來很嚴重的性能問題,因爲這樣會使得 CPU 和 GPU 的並行性完全喪失,CPU 會在 GPU 完成之前一直處於空等的狀態。因此,如果能夠有一種機制,在不需要對 Buffer 進行讀寫 的時候,大家各幹各的;當需要對 Buffer 進行讀寫的時候,可以知道此時 Buffer 在 GPU 的狀態,必要的時候等一下,就不會有上面的問題了。

fence 就是這樣的同步機制,如它直譯過來的意思一樣——“柵欄”,用來把東西攔住。那麼 fence 是要攔住什麼東西呢?就是前面提到的 Buffer 了。Buffer 在整個繪製、合成、顯示的過程中,一直在 CPU,GPU 和 HWC 之前傳遞,某一方要使用 Buffer 之前,需要檢查之前的使用者是否已經移交了 Buffer 的“使用權”。而這裏的“使用權”,就是 fence。當 fence 釋放(即 signal)的時候,說明 Buffer 的上一個使用者已經交出了使用權,對於 Buffer 進行操作是安全的。

fence in Code

在 Android 源碼裏面,fence 的實現總共分爲四部分:

fence driver    同步的核心實現libsync    位於 system/core/libsynclibsync 的主要作用是對 driver 接口的封裝Fence 類    這個 Fence 類位於 frameworks/native/libs/ui/Fence.cpp,主要是對 libsync 進行 C++ 封裝,方便 framework 調用FenceTime 類    這個 FenceTime 是一個工具類,是對 Fence 的進一步封裝,提供兩個主要的接口——isValid() 和 getSignalTime(),主要作用是針對需要多次查詢 fence 的釋放時間的場景(通過調用 Fence::getSignalTime() 來查詢 fence 的釋放時間)。通過對 Fence 進行包裹,當第一次調用 FenceTime::getSignalTime() 的時候,如果 fence 已經釋放,那麼會將這個 fence 的釋放時間緩存起來,然後下次再調用 FenceTime::getSignal() 的時間,就能將緩存起來的釋放時間直接返回,從而減少對 Fence::getSignalTime() 不必要的調用(因爲 fence 釋放的時間不會改變)。

fence in Android

在 Android 裏面,總共有三類 fence —— acquire fence,release fence 和 present fence。其中,acquire fence 和 release fence 隸屬於 Layer,present fence 隸屬於幀(即 Layers):

acquire fence    前面提到, App 將 Buffer 通過 queueBuffer() 還給 BufferQueue 的時候,此時該 Buffer 的 GPU 側其實是還沒有完成的,此時會帶上一個 fence,這個 fence 就是 acquire fence。當 SurfaceFlinger/ HWC 要讀取 Buffer 以進行合成操作的時候,需要等 acquire fence 釋放之後才行。release fence    當 App 通過 dequeueBuffer() 從 BufferQueue 申請 Buffer,要對 Buffer 進行繪製的時候,需要保證 HWC 已經不再需要這個 Buffer 了,即需要等 release fence signal 才能對 Buffer 進行寫操作。present fence    present fence 在 HWC1 的時候稱爲 retire fence,在 HWC2 中改名爲 present fence。當前幀成功顯示到屏幕的時候,present fence 就會 signal。

原理分析

簡單版

現在來看一下通過 dumpsys SurfaceFlinger --latency Layer-name 計算 Layer fps 的原理。dumpsys 的調用流程就不贅述了,最終會走到 SurfaceFlinger::doDump():

status_t SurfaceFlinger::doDump(int fd, const DumpArgs& args,
                                bool asProto) NO_THREAD_SAFETY_ANALYSIS {


    ...
        static const std::unordered_map<std::string, Dumper> dumpers = {
                ......
                {"--latency"s, argsDumper(&SurfaceFlinger::dumpStatsLocked)},
                ......
        };

從這裏可以看到,我們在執行 dumpsys SurfaceFlinger 後面加的那些 --xxx 參數最終都會在這裏被解析,這裏咱們是 --latency,因此看 SurfaceFlinger::dumpStatsLocked

void SurfaceFlinger::dumpStatsLocked(const DumpArgs& args, std::string& result) const {
    StringAppendF(&result, "%" PRId64 "\n", getVsyncPeriod());


    if (args.size() > 1) {
        const auto name = String8(args[1]);
        mCurrentState.traverseInZOrder([&](Layer* layer) {
            if (name == layer->getName()) {
                layer->dumpFrameStats(result);
            }
        });
    } else {
        mAnimFrameTracker.dumpStats(result);
    }
}

從這裏就能夠看到,這裏先會打印當前的 VSYNC 間隔,然後遍歷當前的 Layer,然後逐個比較 Layer 的名字,如果跟傳進來的參數一致的話,那麼就會開始 dump layer 的信息;否則命令就結束了。因此,很多人會遇到這個問題:

❔ 爲什麼執行了這個命令卻只打印出一個數字?

✔ 其實這個時候你應該去檢查你的 Layer 參數是否正確。

接下來 layer->dumpFrameStats() 會去調 FrameTrack::dumpStats()

void FrameTracker::dumpStats(std::string& result) const {
    Mutex::Autolock lock(mMutex);
    processFencesLocked();


    const size_t o = mOffset;
    for (size_t i = 1; i < NUM_FRAME_RECORDS; i++) {
        const size_t index = (o+i) % NUM_FRAME_RECORDS;
        base::StringAppendF(&result, "%" PRId64 "\t%" PRId64 "\t%" PRId64 "\n",
                            mFrameRecords[index].desiredPresentTime,
                            mFrameRecords[index].actualPresentTime,
                            mFrameRecords[index].frameReadyTime);
    }
    result.append("\n");
}

NUM_FRAME_RECORDS 被定義爲 128,因此輸出的數組有 127 個。每組分別有三個數字—— desiredPresentTimeactualPresentTimeframeReadyTime,每個數字的意義分別是:

desiredPresentTime     下一個 HW-VSYNC 的時間戳actualPresentTime     retire fence signal 的時間戳frameReadyTime     acquire fence signal 的時間戳

結合前面對 present fence 的描述就可以看出 dumpsys SurfaceFlinger --latency 計算 fps 的原理:

從 dumpsys SurfaceFlinger --latency 獲取到最新 127 幀的 present fence 的 signal time,結合前面對於 present fence 的說明,當某幀 present fence 被 signal 的時候,說明這一幀已經被顯示到屏幕上了。因此,我們可以通過判斷一秒內有多少個 present fence signal 了,來反推出一秒內有多少幀被刷到屏幕上,從而計算出 fps。

複雜版

我們已經知道了 fps 計算的原理了,但是呢,小朋友,你是否有很多問號?

這個 actualPresentTime 是從哪來的?假設要統計 fps 的 Layer 沒有更新,但是別的 Layer 更新了,這種情況下 present fence 也會正常 signal,那這樣計算出來的 fps 是不是不準啊?

爲了解答這些問題,我們還得接着看。

前面已經提到計算 fps 的時候使用的是第二個數值,因此後面的文章着重分析這個 actualPresentTime。那麼 actualPresentTime 是在哪裏賦值的呢?實際賦值的位置是在 FrameTracker::dumpStats() 調用的一個子函數——processFencesLocked()

void FrameTracker::processFencesLocked() const {
    FrameRecord* records = const_cast<FrameRecord*>(mFrameRecords);
    int& numFences = const_cast<int&>(mNumFences);


    for (int i = 1; i < NUM_FRAME_RECORDS && numFences > 0; i++) {
        size_t idx = (mOffset+NUM_FRAME_RECORDS-i) % NUM_FRAME_RECORDS;
        ...
        const std::shared_ptr<FenceTime>& pfence =
                records[idx].actualPresentFence;
        if (pfence != nullptr) {
            // actualPresentTime 是在這裏賦值的
            records[idx].actualPresentTime = pfence->getSignalTime();
            if (records[idx].actualPresentTime < INT64_MAX) {
                records[idx].actualPresentFence = nullptr;
                numFences--;
                updated = true;
            }
        }
        ......

其中,FrameRecord 的完整定義如下:

struct FrameRecord {
     FrameRecord() :
         desiredPresentTime(0),
         frameReadyTime(0),
         actualPresentTime(0) {}
     nsecs_t desiredPresentTime;
     nsecs_t frameReadyTime;
     nsecs_t actualPresentTime;
     std::shared_ptr<FenceTime> frameReadyFence;
     std::shared_ptr<FenceTime> actualPresentFence;
};

從上面的代碼可以看出,actualPresentTime 是調用 actualPresentFence 的 getSignalTime() 賦值的。而 actualPresentFence 是通過 setActualPresentFence() 賦值的:

void FrameTracker::setActualPresentFence(
        std::shared_ptr<FenceTime>&& readyFence) {
    Mutex::Autolock lock(mMutex);
    mFrameRecords[mOffset].actualPresentFence = std::move(readyFence);
    mNumFences++;
}

setActualPresentFence() 又是經過下面的調用流程最終被調用的:

SurfaceFlinger::postComposition()
  \_ BufferLayer::onPostCompostion()

這裏重點看一下 SurfaceFlinger::postComposition()

void SurfaceFlinger::postComposition()
{
    ......
    mDrawingState.traverseInZOrder([&](Layer* layer) {
        bool frameLatched =
                layer->onPostComposition(displayDevice->getId(), glCompositionDoneFenceTime,
                                         presentFenceTime, compositorTiming);
    ......

回憶一下我們前面的問題:

❔ 假設要統計 fps 的 Layer 沒有更新,但是別的 Layer 更新了,這種情況下 present fence 也會正常 signal,那這樣計算出來的 fps 是不是不準啊?

答案就在 mDrawingState,在 Surfacelinger 中有兩個用來記錄當前系統中 Layers 狀態的全局變量:

mDrawingState    mDrawingState 代表的是上次 “drawing” 時候的狀態mCurrentState    mCurrentState 代表的是當前的狀態因此,如果當前 Layer 沒有更新,那麼是不會被記錄到 mDrawingState 裏的,因此這一次的 present fence 也就不會被記錄到該 Layer 的 FrameTracker 裏的 actualPresentTime 了。

再說回來, SurfaceFlinger::postComposition() 是 SurfaceFlinger 合成的最後階段。presentFenceTime 就是前面的 readyFence 參數了,它是在這裏被賦值的:

mPreviousPresentFences[0] = mActiveVsyncSource
        ? getHwComposer().getPresentFence(*mActiveVsyncSource->getId())
        : Fence::NO_FENCE;
auto presentFenceTime = std::make_shared<FenceTime>(mPreviousPresentFences[0]);

而 getPresentFence() 這個函數,就把這個流程轉移到了 HWC 了:

sp<Fence> HWComposer::getPresentFence(DisplayId displayId) const {
    RETURN_IF_INVALID_DISPLAY(displayId, Fence::NO_FENCE);
    return mDisplayData.at(displayId).lastPresentFence;
}

至此,我們一路輾轉,終於找到了這個 present fence 的真身,只不過這裏它還蒙着一層面紗,我們需要在看一下這個 lastPresentFence 是在哪裏賦值的,這裏按照不同的合成方式位置有所不同:

DEVICE 合成

DEVICE 合成的 lastPresentFence 是在 HWComposer::prepare() 裏賦值:

status_t HWComposer::prepare(DisplayId displayId, const compositionengine::Output& output) {
    ......
    if (!displayData.hasClientComposition) {
        sp<Fence> outPresentFence;
        uint32_t state = UINT32_MAX;
        error = hwcDisplay->presentOrValidate(&numTypes, &numRequests, &outPresentFence , &state);
        if (error != HWC2::Error::HasChanges) {
            RETURN_IF_HWC_ERROR_FOR("presentOrValidate", error, displayId, UNKNOWN_ERROR);
        }
        if (state == 1) { //Present Succeeded.
            ......
            displayData.lastPresentFence = outPresentFence;

經常看 systrace 的同學對這個函數絕對不會陌生,就是 systrace 裏面 SurfaceFlinger 的那個 prepare()

Systrace 中的 prepare

這個函數非常重要,它通過一系列的調用:

HWComposer::prepare()
  \_ Display::presentOrValidate()
       \_ Composer::presentOrValidateDisplay()
            \_ CommandWriter::presentOrvalidateDisplay()

最終通過 HwBinder 通知 HWC 的 Server 端開始進行 DEVICE 合成,Server 端在收到 Client 端的請求以後,會返回給 Client 端一個 present fence(時刻記住,fence 用於跨環境的同步,例如這裏就是 Surfacelinger 和 HWC 之間的同步)。然後當下一個 HW-VSYNC 來的時候,會將合成好的內容顯示到屏幕上並且將該 present fence signal,標誌着這一幀已經顯示在屏幕上了。

GPU 合成

GPU 合成的 lastPresentFence 是在 presentAndGetPresentFences() 裏賦值:

status_t HWComposer::presentAndGetReleaseFences(DisplayId displayId) {
    ......
    displayData.lastPresentFence = Fence::NO_FENCE;
    auto error = hwcDisplay->present(&displayData.lastPresentFence);

後面的流程就跟 DEVICE 合成類似了,Display::present() 最終也會經過一系列的調用,通過 HwBinder 通知 HWC 的 Server 端,調用 presentDisplay() 將合成好的內容顯示到屏幕上。

總結

說了這麼多,一句話總結計算一個 App 的 fps 的原理就是:

統計在一秒內該 App 往屏幕刷了多少幀,而在 Android 的世界裏,每一幀顯示到屏幕的標誌是:present fence signal 了,因此計算 App 的 fps 就可以轉換爲:一秒內 App 的 Layer 有多少個有效 present fence signal 了(這裏有效 present fence 是指,在本次 VSYNC 中該 Layer 有更新的 present fence)

尾巴

這篇文章在二月份其實就已經完成了一多半了,但是一直拖到了五月才最終寫完,因爲其中涉及到很多我不知道的知識,例如 HWC。這塊領域涉及到硬件,文檔其實不多。因此在寫的過程會變得異常痛苦,很多東西不懂,我也不知道自己寫的東西究竟對不對,就需要花很多時間進行多方求證,找很多大佬提問。很多時候會卡在某一個地方很久,甚至會萌生隨便寫點糊弄過去算了的想法。而且,寫到什麼程度也很難拿捏,寫淺了我自己過意不去,感覺對不起各位關注的讀者;寫深了我自己也是寫不下去,畢竟這個領域確實之前沒有接觸過。不過好在這個過程中有很多大佬給我提供了很大的幫助,在此對在這幾月中給我答疑解惑的各位大佬表示衷心的感謝。

寫作是一個孤獨的旅程,感謝各位大佬的指路,感謝各位讀者的關注,你們是星星太陽和月亮,陪着我一直寫下去,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章