1. 程式人生 > >Janky frames 是如何計算出來的

Janky frames 是如何計算出來的

背景

最近在做一些效能監控的工作,其中線下監控fps這一項,經過調研,最終採用dumpsys gfxinfo的方式。

在6.0+的手機中執行如下命令,

adb shell dumpsys gfxinfo 包名 

可以得到一些log:

Applications Graphics Acceleration Info:
Uptime: 3820706382 Realtime: 3903615964

** Graphics info for pid 427 [包名] **

Stats since: 3820661771494092ns
Total frames rendered: 201
//重點關注物件
Janky frames: 76 (37.81%) 50th percentile: 6ms 90th percentile: 19ms 95th percentile: 61ms 99th percentile: 300ms Number Missed Vsync: 14 Number High input latency: 0 Number Slow UI thread: 17 Number Slow bitmap uploads: 5 Number Slow issue draw commands: 60 ........ //重點關注物件 ActivityName/[email protected]
(visibility=0) //每個Activity的每一幀的原始資料,包含每個階段的時間戳 ---PROFILEDATA--- Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted, 0,3820697872348436,3820697872348436,9223372036854775807,0,3820697872662836,3820697872693045,3820697872814399,3820697872932628,3820697873194607,3820697873228461,3820697873328982,3820697873869086,3820697876514920, 0,3820697889204506,3820697889204506,9223372036854775807,0,3820697889517524,3820697889547211,3820697889672211,3820697889801899,3820697890120649,3820697890156586,3820697890267524,3820697890787836,3820697892957628, 0,3820697906060465,3820697906060465,9223372036854775807,0,3820697906622211,3820697906650857,3820697906761795,3820697906888357,3820697907180024,3820697907215961,3820697907325857,3820697907809190,3820697909209190, 0,3820697922916378,3820697922916378,9223372036854775807,0,3820697923237315,3820697923265440,3820697923397732,3820697923533149,3820697923806586,3820697923837315,3820697923936795,3820697924388878,3820697927055024, 0,3820697939772285,3820697939772285,9223372036854775807,0,3820697940389920,3820697940418565,3820697940532628,3820697940652420,3820697940915961,3820697940948774,3820697941046690,3820697941496170,3820697943844086, 0,3820697956628358,3820697956628358,9223372036854775807,0,3820697956922732,3820697956952940,3820697957073774,3820697957197732,3820697957457107,3820697957490961,3820697957583149,3820697958036795,3820697959429503, ............

其中有一項名為:Janky frames的資料引起了我們的興趣。

Janky frames該如何理解呢?參考[官方文件1 ]的說明,似乎就是掉幀的數量。

可如果按照掉幀的數量來理解,這份log顯示的掉幀率高達37.81%,一個app如果近40%的幀都被skip,使用者不可能毫無感知。

但在我們測試時,沒有感覺到明顯的卡頓。(且根據原始資料,用另外一套計算方式,算出的幀率fps值也與掉幀率的百分比矛盾)

但這Janky frames畢竟是官方adb命令給出的值,具有一定的權威性,於是我們開始自我懷疑,
* 是我們的眼睛沒有看出卡頓?
* 是我們計算幀率fps的方式出現了問題?
* …

由於官方在log中並未給出實際fps的值,於是為了探究問題出在哪裡,也為了參考官方的計算標準,即如何判定一幀出現了janky,我便把黑手伸向了無辜的原始碼,畢竟原始碼之下,了無祕密。

遂,現在的目標是:
* 找到adb shell dumpsys gfxinfo的原始碼
* 找到原始碼裡關於Janky frame的計算方法

找到gfxinfo原始碼

經過搜尋,在Android dumpsys工具分析文中中得知,當我們執行adb shell dumpsys後,根據後面不同的引數,例如meminfogfxinfo,實際上是通過ServiceManager->checkService(services[i])方法,從ServiceManager中取出對應服務的Binder物件,並最終通過service->dump(STDOUT_FILENO, args)呼叫對應服務Binder物件的dump()方法執行具體命令。

這些系統服務的註冊,是在AMS(ActivityManagerService.java)裡的setSystemProcess()裡完成的,

    public void setSystemProcess() {
        try {
            //在ServiceManager中註冊服務
            //"activity";
            ServiceManager.addService(Context.ACTIVITY_SERVICE, this, true);
            ServiceManager.addService(ProcessStats.SERVICE_NAME, mProcessStats);
            ServiceManager.addService("meminfo", new MemBinder(this));
            ServiceManager.addService("gfxinfo", new GraphicsBinder(this));
            ServiceManager.addService("dbinfo", new DbBinder(this));
            ...
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException(
                    "Unable to find android system package", e);
        }
    }

可以看到,我們熟悉的 “activity”、”meminfo”以及本文的主角”gfxinfo”都在其中註冊。

java層原始碼

順藤摸瓜,我們看看GraphicsBinder這個類以及它的dump()方法:

    static class GraphicsBinder extends Binder {
        ActivityManagerService mActivityManagerService;
        GraphicsBinder(ActivityManagerService activityManagerService) {
            mActivityManagerService = activityManagerService;
        }

        @Override
        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            if (!DumpUtils.checkDumpAndUsageStatsPermission(mActivityManagerService.mContext,
                    "gfxinfo", pw)) return;
            //呼叫ams的dumpGraphicsHardwareUsage()方法
            mActivityManagerService.dumpGraphicsHardwareUsage(fd, pw, args);
        }
    }

dump()方法很簡單,僅僅驗證許可權後呼叫ams的dumpGraphicsHardwareUsage()方法,繼續跟進:

    final void dumpGraphicsHardwareUsage(FileDescriptor fd,
            PrintWriter pw, String[] args) {
        //根據args引數,引數裡包含程序名 或者程序id,得到指定程序。 如果args引數裡不包含程序名,則得到所有程序
        ArrayList<ProcessRecord> procs = collectProcesses(pw, 0, false, args);
        //沒有符合條件的程序時的輸出
        if (procs == null) {
            pw.println("No process found for: " + args[0]);
            return;
        }
        //執行命令時的時間
        long uptime = SystemClock.uptimeMillis();
        long realtime = SystemClock.elapsedRealtime();
        pw.println("Applications Graphics Acceleration Info:");
        pw.println("Uptime: " + uptime + " Realtime: " + realtime);
        //迴圈程序列表
        for (int i = procs.size() - 1 ; i >= 0 ; i--) {
            ProcessRecord r = procs.get(i);
            if (r.thread != null) {
                pw.println("\n** Graphics info for pid " + r.pid + " [" + r.processName + "] **");
                pw.flush();
                try {
                    TransferPipe tp = new TransferPipe();
                    try {
                        //重點,執行每個程序的ApplicationThread的dumpGfxInfo()方法
                        r.thread.dumpGfxInfo(tp.getWriteFd(), args);
                        tp.go(fd);
                    } finally {
                        tp.kill();
                    }
                } catch (IOException e) {
                    pw.println("Failure while dumping the app: " + r);
                    pw.flush();
                } catch (RemoteException e) {
                    pw.println("Got a RemoteException while dumping the app " + r);
                    pw.flush();
                }
            }
        }
    }

可以看到,我們關心的核心輸出(Janky frames部分)以程序區分,並在ApplicationThread.dumpGfxInfo()方法中輸出。
ApplicationThreadActivityThread.java中,繼續跟進:

        @Override
        public void dumpGfxInfo(ParcelFileDescriptor pfd, String[] args) {
            //jni  ,janky frames輸出就在其中
            dumpGraphicsInfo(pfd.getFileDescriptor());
            // java方法,輸出 Profile data in ms: 後面的部分
            WindowManagerGlobal.getInstance().dumpGfxInfo(pfd.getFileDescriptor(), args);
            IoUtils.closeQuietly(pfd);
        }

    // ------------------ Regular JNI ------------------------
    private native void dumpGraphicsInfo(FileDescriptor fd);

檢視WindowManagerGlobal.dumpGfxInfo()方法:

    public void dumpGfxInfo(FileDescriptor fd, String[] args) {
        FileOutputStream fout = new FileOutputStream(fd);
        PrintWriter pw = new FastPrintWriter(fout);
        try {
            synchronized (mLock) {
                final int count = mViews.size();

                pw.println("Profile data in ms:");

                for (int i = 0; i < count; i++) {
                    ViewRootImpl root = mRoots.get(i);
                    String name = getWindowName(root);
                    pw.printf("\n\t%s (visibility=%d)", name, root.getHostVisibility());

                    ThreadedRenderer renderer =
                            root.getView().mAttachInfo.mThreadedRenderer;
                    if (renderer != null) {
                        renderer.dumpGfxInfo(pw, fd, args);
                    }
                }

                pw.println("\nView hierarchy:\n");

                int viewsCount = 0;
                int displayListsSize = 0;
                int[] info = new int[2];

                for (int i = 0; i < count; i++) {
                    ViewRootImpl root = mRoots.get(i);
                    root.dumpGfxInfo(info);

                    String name = getWindowName(root);
                    pw.printf("  %s\n  %d views, %.2f kB of display lists",
                            name, info[0], info[1] / 1024.0f);
                    pw.printf("\n\n");

                    viewsCount += info[0];
                    displayListsSize += info[1];
                }

                pw.printf("\nTotal ViewRootImpl: %d\n", count);
                pw.printf("Total Views:        %d\n", viewsCount);
                pw.printf("Total DisplayList:  %.2f kB\n\n", displayListsSize / 1024.0f);
            }
        } finally {
            pw.flush();
        }
    }

可知,其中輸出的是”Profile data in ms:”後面部分的log,所以,我們關心的部分就在JNI裡了。

C層原始碼

看到JNI我是抗拒的,本科時學的那些C、C++早已記不太清,一想到要看C層的原始碼,就覺得頭大。原本想溜,但是轉念一想,我只需要關注它對於”Janky frame”的計算方式,無外乎那些數學運算。只要重點關注函式呼叫處,搜尋關鍵字,說不定可以找到答案。於是,繼續跟進。

由於涉及C層的原始碼在AndroidStudio中檢視不了,下面的分析使用檢視framework原始碼網站進行。我全域性搜尋了關鍵字dumpGraphicsInfo,找到函式定義處:


進入android_view_DisplayListCanvas.cpp檢視():


搜尋dumpGraphicsMemory


進入RenderProxy.cpp後,

由於對c++不是很懂,這一塊的程式碼不是很懂,但是從全域性搜尋只有三處呼叫,而且從jankTracker的字樣上可以看出(這一步有一些連蒙帶猜),這裡應該是正確的方向,繼續跟進:
JankTracker.h檔案中:

於是我們進入JankTracker.cpp中檢視dumpData()方法的具體實現:


看到這裡我是既興奮又痛苦。興奮的是我找到了最終log對應的輸出之處,痛苦的是,這裡僅僅是將ProfileData->jankFrameCount欄位輸出,看來革命之路還長,還要找到賦值的地方。

jankFrameCount 賦值之處

全域性搜尋->jankFrameCount呼叫之處,:


發現僅在JankTracker.cpp中使用到,在addFrame()函式中會遞增:


可以看出,如果一幀的時間如果小於mFrameInterval,則return,那麼jankFrameCount不會遞增即 每一幀的時間大於等於mFrameInterval,就是Janky frame

看來我們離答案已經很接近了,那麼mFrameInterval是多少呢?
搜尋mFrameInterval是在setFrameInterval()中賦值的:


setFrameInterval()在JankTracker初始化時呼叫:

根據官方文件1以及官方視訊why60fps,重新整理頻率fps是60。所以可得frameIntervalNanos約等於16.67ms.

結論

至此我們可以得出結論,官方衡量Janky frames的標準:一幀的時間超過16.67ms。

想法

注:以下想法目前還沒有原始碼撐腰,並不一定正確,如有錯誤以及知情大佬,煩請指正,謝謝

有上述結論可以看出 Janky frames確實代表了這一幀的完整繪製時間太久,出現了問題,

那麼回到我們文首的問題,某些頁面的Janky frames高達近37.81%,為何我們沒有感到卡頓?以及為何算出來的fps並沒有低於37.2= 60*(1-0.38)?

關於這個問題,經過討論,有以下暫時的想法:

  • Android 5.0以後,加入了RenderThread,用於分擔UI Thread的部分繪製工作。即一幀的完整繪製時間 是由UIThread和RenderThread上的耗時相加得到的。
  • UI Thread在處理完input animation以及部分draw的工作後,將剩餘繪製工作交於RenderThread,UI Thread此時可以繼續處理下一個VSYNC到來時的工作。
  • RenderThread 以及三緩衝機制

可以看出B先導致了一次視覺上的jank,C理論上也是jank的(相加時間超過了16.67ms),但是由於此時螢幕上顯示的是B,C雖然delay了一幀,但是由於C之前的B已經delay了一幀,所以C看起來仍然是緊跟著B顯示在螢幕上,而且A順利的在16.67ms完成了任務,緊跟著C繼續繪製了,則使用者在視覺上只少看到了一幀。

所以我們的想法是:

在Android5.0+,Janky frames 並不代表使用者視覺上的,顯示在螢幕上的丟幀率,但是它可以代表有問題的幀率

即這些幀有問題,但最終由於三緩衝機制的背鍋,部分幀沒有最終影響到使用者,

所以實際上的fps值會高於 60*(1-掉幀率).

這個問題後面也會繼續跟進,做到理據服。

相關推薦

Janky frames 是如何計算出來

背景 最近在做一些效能監控的工作,其中線下監控fps這一項,經過調研,最終採用dumpsys gfxinfo的方式。 在6.0+的手機中執行如下命令, adb shell dumpsys gfxinfo 包名 可以得到一些log: A

ggplot2 提取stat計算出來的數據

1.8 xtra transform xlabel oba 技術 fill 4.5 mit 使用ggplot2 繪圖時,我們只需要提供原始數據就可以了,ggplot2 內置了許多的計算函數,來幫助我們計算對應的數值。 最典型的的,當使用geom_boxplot 繪制箱線圖時

學雲計算出來多少錢?學Linux怎麽樣?

雲計算雲計算現在可以說是越來越火熱了,各大培訓班也猶如雨後春筍般湧現,花式的廣告也弄得人心癢癢,都知道雲計算前途無量,可雲計算培訓出來多少錢呢?說再花哨都是虛的,真實的數據才是真正有說服力的證據。這次,×××老師就自豪的給大家介紹一下我麽×××教育的情況。 眾所周知,“做真實的自己,用良心做教育”一直以來是×

每個zone的low memory是怎麼計算出來

deferred_init_memmap -->deferred_free_range     6801 /*6802  * Initialise min_free_kbytes.6803  *6804  * For small machines

應用性能指數(APDEX)是如何計算出來的?

定義 通過 健康 統一 threshold 希望 form pla 量化 在應用性能管理領域聚合指標是一種常見手段,主要是用來把成百上千的指標通過某種計算方法聚合成一個或幾個指標,用來反映應用的整體健康狀態。在這些聚合指標中,比較常見的是:APDEX應用性能指數。應用性能指

Java——在一個字串中查詢一個子串,計算出來這個子串在字串中出現的次數。

引入包:import java.util.Scanner;main函式:public static void main(String[] args){Scanner s = new Scanner(Sy

[python]My Unique JsonDiff演算法——如何計算2個json串之間的差距並Diff出來(一):編輯距離(Levenshtein)演算法

    啊啊,年底忙著簽證什麼的,好久沒寫日誌啦。。。。新年到來,整點乾貨出來給大家~~順便為自己考試和申請學校攢點人品~~     之前實習的時候,因為實習公司的業務需求,需要一個比對json字串差異的演算法,然而我在網上查了很久的資料,發現竟然沒有現成

計算機中地址和記憶體大小的計算和編譯出來的資料段

由地址計算記憶體大小(消除模糊認知)     在計算機中一個地址代表一個位元組的記憶體的位置,即這個byte的門牌號,所以如果給出地址空間的起始地址是可以計算出記憶體大小的,比如STM32中Flash可程式設計的地址是從0x0800 0000開始到0x0801FFFF結束的所以記憶體大小的計算過程如下:

用遞迴的方法把一個無符號整數的每一位數字單獨寫出來,並且計算出每一位加起來的和。

這個題的具體含義是什麼呢? 例如: 給出一個無符號整數:1234     一千兩百三十四; 然後變成:1   2   3   4; 再把它們加起來:10; 所以很簡單的一道題,但是我們要用遞迴的思想寫

計算從今天算起,150天之後是幾月幾號,並格式化成xxxx年xx月x日的形式打印出來

提示:呼叫Calendar類的add方法計算150天之後的日期 呼叫Calendar類的getTime方法返回Date型別物件使用Full格式的DateFormat物件,呼叫format方法格式化Da

計算兩幅圖片的farneback 稠密光流,並將結果圖顯示出來的程式

import cv2 import numpy as np import Image import cv2.cv as cv def image_joint(image_list,opt):#opt= vertical ,horizon image_num=len

計算出當前繪製出來的字串寬度和高度

寬度: 方法1:Paint paint= new Paint(); Rect rect = new Rect();//返回包圍整個字串的最小的一個Rect區域paint.getTextBounds(str, 0, 1, rect); strwid = rect.width

Yii2中後臺用前臺的代碼設置驗證碼顯示不出來

font 前後臺 cnblogs 模板 alt 技術分享 size 不出 image 我說的是直接修改advanced模板。細心人會發現模板裏在contact裏有,登錄也想要就仿照contact中的做法。前臺好了,後臺登錄也要驗證碼,就把前臺代碼拿過來,可惜前後臺的Site

計算到底是個什麽?

雲計算 在沒有雲計算沒有GPS的時代。每到陌生的地方總要準備一個當地的地圖。時常會遇到拿著地圖向當地人問路的情況。而現在我們只需要一部手機,就可以擁有一張全新的詳細的當地地圖。還能直觀的了解天氣情況,交通情況等信息。復雜的路況信息,周邊的美食、景點、酒店、休閑娛樂、加油站、公交站….等等的一

mysql 計算生日

代碼 date() log 根據 sql font blog ediff ont 生日(DATE) 計算方法1: YEAR(CURDATE())-YEAR(birthday)-(RIGHT(CURDATE(),5)<RIGHT(birthday,5)) 計算方法2

計算程序的內存和占比

程序 odin main pre == ret 內存占用 put 列表 1 #!/usr/bin/env python 2 # _*_ coding:UTF-8 _*_ 3 # 收集程序所占用的物理內存大小,占所有物理內存的比例 4 # OS: Centos 6.

HDOJ2438:Turn the corner(計算幾何 + 三分)

scan can 計算 closed 4.5 pri cross cli idt Problem Description Mr. West bought a new car! So he is travelling around the city.One day he c

計算的理解

存儲 clas 傳遞 喜歡 升級 自動 color 雲計算 簡單 雲計算是一種通過網絡以服務的方式提供動態可伸縮的虛擬化的資源的計算模式。舉個例子,你要做個網站,希望有一臺獨立的服務器,以前你可能得自行購買一臺服務器並托管在IDC機房,不僅得花很多錢買服務器,而且每年要花很

JavaScript基礎 substr(2, 3) 2是起始的index的值 3是提出來3個字符

subst bstr 博文 htm bst firefox 傳智播客 src 部分 鎮場詩:    清心感悟智慧語,不著世間名與利。學水處下納百川,舍盡貢高我慢意。    學有小成返哺根,願鑄一良心博客。誠心於此寫經驗,願見文者得啟發。——————————————————

SQL必知必會 -------- 通配符、計算字段、函數

提取 mar 第8章 column round vendor 方法 多少 頁面 1.LIKE操作符 1.1百分號(%)通配符 SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE ‘Fish%‘