Android性能優化之GraphicsStatsService(1)

1.概述

GraphicsStatsService是Android M(6.0)之後Google加入的用於收集彙總Android系統的渲染剖面數據(profile data),主要途徑是經過容許渲染線程請求匿名共享存儲緩衝(ashmem buffer)來存放它們的統計信息來實現的。這篇文章旨在分析GraphicsStatsService的工做流程和這些統計信息的前因後果。css

首先來看下GraphicsStatsService都收集了哪些信息。經過adb shell dumpsys graphicsstats 能夠輸出GraphicsStatsService收集的信息,如下是在小米手機上執行該命令時輸出的信息:java

Package: com.android.systemui
Stats since: 23494814317ns
Total frames rendered: 132008
Janky frames: 8913 (6.75%)
90th percentile: 12ms
95th percentile: 19ms
99th percentile: 38ms
Number Missed Vsync: 1954
Number High input latency: 279
Number Slow UI thread: 2704
Number Slow bitmap uploads: 454
Number Slow issue draw commands: 5408android

Package: com.miui.systemAdSolution
Stats since: 234903483403ns
Total frames rendered: 44
Janky frames: 19 (43.18%)
90th percentile: 53ms
95th percentile: 57ms
99th percentile: 113ms
Number Missed Vsync: 3
Number High input latency: 2
Number Slow UI thread: 6
Number Slow bitmap uploads: 11
Number Slow issue draw commands: 10shell

Package: android
Stats since: 272814918805ns
Total frames rendered: 369
Janky frames: 16 (4.34%)
90th percentile: 13ms
95th percentile: 15ms
99th percentile: 31ms
Number Missed Vsync: 2
Number High input latency: 0
Number Slow UI thread: 9
Number Slow bitmap uploads: 2
Number Slow issue draw commands: 11數組

Package: com.miui.personalassistant
Stats since: 295433832807ns
Total frames rendered: 8
Janky frames: 5 (62.50%)
90th percentile: 85ms
95th percentile: 85ms
99th percentile: 85ms
Number Missed Vsync: 3
Number High input latency: 1
Number Slow UI thread: 5
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 1數據結構

...........app

能夠看到輸出不少應用的渲染信息,以包名做爲區分。其中Stats since表示該應用的統計信息是從系統開機多長時間(納秒)後開始統計的Total frames表示一共繪製了多少幀,Janky frames表示有多少幀是卡頓的,90th percentile、95th percentile、99th percentile分別表示90%、95%、99%的幀是在多少毫秒內完成的;最後的五個指標表示卡頓的具體緣由及卡頓的幀數(一個幀可能有多個卡頓的緣由),具體的解釋能夠看第5章的第4小結。這些信息能夠幫助應用開發者分析其應用的卡頓狀況,也能夠幫助系統開發瞭解整個系統的性能狀況。ide

那麼這GraphicsStatsService服務運做機制是如何的呢?這些統計數據都是怎麼收集的?下面讓咱們一步步來探索,首先看一下GraphicsStatsService都長啥樣。函數

2.GraphicsStatsService類的解析

GraphicsStatsService 類文件位於frameworks/base/services/core/java/com/android/server /GraphicsStatsService.java,系統不少核心的服務都位於該目錄下。post

1)爲進程分配存儲統計信息的buffer

該類實現了IGraphicsStats接口,本質上是一個binder,IGraphicsStats接口經過AIDL實現,相應的文件是frameworks/base/core/java/android/view/IGraphicsStats.aidl。裏面只定義了一個方法:

interface IGraphicsStats {
    ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token);
}

所以,requestBufferForProcess方法也就是GraphicsStatsService的核心方法之一,顧名思義,該方法是給由packageName指定的進程分配buffer,並返回指向該buffer的文件描述符,具體代碼很少,列舉在下面:

@Override
    public ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token)
            throws RemoteException {
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();
        ParcelFileDescriptor pfd = null;
        long callingIdentity = Binder.clearCallingIdentity();
        try {
            if (!isValid(uid, packageName)) {
                throw new RemoteException("Invalid package name");
            }
            synchronized (mLock) {
                pfd = requestBufferForProcessLocked(token, uid, pid, packageName);
            }
        } finally {
            Binder.restoreCallingIdentity(callingIdentity);
        }
        return pfd;
    }

在這段代碼中,首先檢驗包名的合法性,這個主要是經過比較token中的getCallingUid和packageName對應的uid是否相等來實現的,因此咱們要傳兩個相關聯的packageName和token進來。

隨後,若是合法性檢驗經過,則調用requestBufferForProcessLocked分配buffer,這個方法又調用了fetchActiveBuffersLocked

private ActiveBuffer fetchActiveBuffersLocked(IBinder token, int uid, int pid,
            String packageName) throws RemoteException {
        int size = mActive.size();
        for (int i = 0; i < size; i++) {
            ActiveBuffer buffers = mActive.get(i);
            if (buffers.mPid == pid
                    && buffers.mUid == uid) {
                return buffers;
            }
        }
        // Didn't find one, need to create it
        try {
            ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName);
            mActive.add(buffers);
            return buffers;
        } catch (IOException ex) {
            throw new RemoteException("Failed to allocate space");
        }
    }

這段代碼中,首先根據uid和pid嘗試從mActive取buffer,若是取到則直接返回,不然新建立一個ActiveBuffer加入mActive並返回引用。能夠看到系統所有分配的buffer是經過mActive來統一管理的,它是一個ArrayList<ActiveBuffer>,而ActiveBuffer則是核心類,它是GraphicsStatsService的一個final內部類,代碼很少,具體以下:

private final class ActiveBuffer implements DeathRecipient {
        final int mUid;
        final int mPid;
        final String mPackageName;
        final IBinder mToken;
        MemoryFile mProcessBuffer;
        HistoricalData mPreviousData;

        ActiveBuffer(IBinder token, int uid, int pid, String packageName)
                throws RemoteException, IOException {
            mUid = uid;
            mPid = pid;
            mPackageName = packageName;
            mToken = token;
            mToken.linkToDeath(this, 0);
            mProcessBuffer = new MemoryFile("GFXStats-" + uid, ASHMEM_SIZE);
            mPreviousData = removeHistoricalDataLocked(mUid, mPackageName);
            if (mPreviousData != null) {
                mProcessBuffer.writeBytes(mPreviousData.mBuffer, 0, 0, ASHMEM_SIZE);
            }
        }

        @Override
        public void binderDied() {
            mToken.unlinkToDeath(this, 0);
            processDied(this);
        }

        void closeAllBuffers() {
            if (mProcessBuffer != null) {
                mProcessBuffer.close();
                mProcessBuffer = null;
            }
        }
    }

在構造方法中,咱們能夠清晰地看到,buffer最終是經過匿名共享內存的一個形式MemoryFile來實現的,而底層是經過JNI來進行讀寫的。另外注意到,該類實現了DeathRecipient接口,意思是死亡收件人,裏面只有一個方法就是binderDied()。在構造方法中,調用了mToken.linkToDeath(this, 0)將本身註冊成爲死亡收件人,當持有該binder(mToken)的進程死亡的時候,就會回調binderDied(),而後在processDied()中作清理工做,自此爲進程請求buffer的Java層過程已經完畢。

2)統計信息的輸出過程

那麼GraphicsStatsService是在哪裏輸出統計信息的呢,那就得看在GraphicsStatsService中的另一個關鍵方法,就是dump(FileDescriptor fd, PrintWriter fout, String[] args),該方法重寫了Binder的同名方法,專門用於輸出渲染的統計信息,以下:

@Override
    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
        synchronized (mLock) {
            for (int i = 0; i < mActive.size(); i++) {
                final ActiveBuffer buffer = mActive.get(i);
                fout.print("Package: ");
                fout.print(buffer.mPackageName);
                fout.flush();
                try {
                    buffer.mProcessBuffer.readBytes(mTempBuffer, 0, 0, ASHMEM_SIZE);
                    ThreadedRenderer.dumpProfileData(mTempBuffer, fd);
                } catch (IOException e) {
                    fout.println("Failed to dump");
                }
                fout.println();
            }
            for (HistoricalData buffer : mHistoricalLog) {
                if (buffer == null) continue;
                fout.print("Package: ");
                fout.print(buffer.mPackageName);
                fout.flush();
                ThreadedRenderer.dumpProfileData(buffer.mBuffer, fd);
                fout.println();
            }
        }
    }

這個方法首先檢查相應的權限(dump數據也是要權限的啊),而後是兩個for循環。

第一個循環遍歷mActive,輸出包名,而後調用buffer.mProcessBuffer.readBytes(mTempBuffer, 0, 0, ASHMEM_SIZE),讀取ASHMEM_SIZE到mTempBuffer,再而後就交給ThreadedRenderer的dumpProfileData(mTempBuffer, fd)去輸出了,GraphicsStatsService本身並無幹什麼事情,這其實也是正常的,畢竟裏面的mTempBuffer的數據格式GraphicsStatsService是不知道的。咱們首先想想,渲染統計信息究竟是誰放進去的?用腳趾頭想想就能夠知道,那確定是負責渲染的那個傢伙啊,它本身的事情它本身最清楚。ThreadedRenderer的這名字看起來就是渲染線程。因此具體的統計信息的輸出仍是得由它老人家來負責(並且咱們猜想也是它放進去的,具體咱們後面再看)。

後面還有一個for,用於輸出HistoricalData(歷史數據嘛,這名字仍是很容易懂得),過程其實和上面差很少。

而後這個dump是如何被調用的呢??回想一下咱們的命令adb shell dumpsys graphicsstats,咱們能夠發現跟dumpsys有關,其執行文件位於系統目錄下的/system/bin/dumpsys,源文件位於frameworks/native/cmds/dumpsys/dumpsys.cpp,關鍵代碼以下

int main(int argc, char* const argv[])
{
    signal(SIGPIPE, SIG_IGN);
    sp<IServiceManager> sm = defaultServiceManager();
    fflush(stdout);
    if (sm == NULL) {
		ALOGE("Unable to get default service manager!");
        aerr << "dumpsys: Unable to get default service manager!" << endl;
        return 20;
    }

    Vector<String16> services;
    Vector<String16> args;
    bool showListOnly = false;
   
    ...


    services.add(String16(argv[1]));
    for (int i=2; i<argc; i++) {
        args.add(String16(argv[i]));

    }

    const size_t N = services.size();

    ...

    for (size_t i=0; i<N; i++) {
        sp<IBinder> service = sm->checkService(services[i]);
        if (service != NULL) {
            
            ...

            int err = service->dump(STDOUT_FILENO, args);
            if (err != 0) {
                aerr << "Error dumping service info: (" << strerror(err)
                        << ") " << services[i] << endl;
            }
        } else {
            aerr << "Can't find service: " << services[i] << endl;
        }
    }

    return 0;
}

首先經過defaultServiceManager取得ServiceManager,經過它能夠取得全部的系統服務,而後咱們輸入的graphicsstats會被輸入到services裏面,sm->checkService(services[i])經過名字取得對應service的引用,最後由service->dump(STDOUT_FILENO, args)完成信息的輸出。這個掉用就是調用Binder裏面的 dump(FileDescriptor fd, String[] args) ,最終調用上面所說的dump(FileDescriptor fd, PrintWriter fout, String[] args)。

如此,GraphicsStatsService類中的代碼已經分析完了,這只是整個流程的開始,下面咱們繼續分析。

3.GraphicsStatsService的啓動流程

首先,GraphicsStatsService做爲系統服務,確定是在實在SystemServer中被啓動的。具體代碼是在/homeframeworks/base/services/java/com/android/server/SystemServer.java的startOtherServices()中,以下:

if (!disableNonCoreServices) {
                ServiceManager.addService(GraphicsStatsService.GRAPHICS_STATS_SERVICE,
                        new GraphicsStatsService(context));
            }

在這裏咱們能夠看到,GraphicsStatsService並非核心服務,若是disableNonCoreServices爲true,那麼它將不被啓動。這樣,服務在開機的時候已經起動了,那麼requestBufferForProcess何時調用呢?

ThreadedRenderer的內部靜態類ProcessInitializer中,有個initGraphicsStats(Context context, long renderProxy) 方法:

private static void initGraphicsStats(Context context, long renderProxy) {
            try {
                IBinder binder = ServiceManager.getService("graphicsstats");
                if (binder == null) return;
                IGraphicsStats graphicsStatsService = IGraphicsStats.Stub
                        .asInterface(binder);
                sProcToken = new Binder();
                final String pkg = context.getApplicationInfo().packageName;
                ParcelFileDescriptor pfd = graphicsStatsService.
                        requestBufferForProcess(pkg, sProcToken);
                nSetProcessStatsBuffer(renderProxy, pfd.getFd());
                pfd.close();
            } catch (Throwable t) {
                Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
            }
        }

咱們能夠看到,這裏一樣是經過ServiceManager取得了graphicsStatsService,而後調用requestBufferForProcess爲進程分配buffer並返回文件描述符pfd,而後經過本地方法nSetProcessStatsBuffer(renderProxy, pfd.getFd()) 將渲染的代理類與文件描述符關聯。好,到此爲止,graphicsStatsService的流程已經完了,下面咱們重點關注Native層渲染統計信息的收集和輸出。先來看簡單的,輸出方面,瞭解其數據結構,再看收集。

4.Native層渲染統計信息的輸出

前面咱們分析java層面的統計信息輸出,分析到了ThreadedRenderer的dumpProfileData(mTempBuffer, fd),下面咱們繼續分析。該方法直接調用了native方法nDumpProfileData,jni層對應的文件是/frameworks/base/core/jni/android_view_ThreadedRenderer.cpp,對應的方法以下:

static void android_view_ThreadedRenderer_dumpProfileData(JNIEnv* env, jobject clazz,
        jbyteArray jdata, jobject javaFileDescriptor) {
    int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor);
    ScopedByteArrayRO buffer(env, jdata);
    if (buffer.get()) {
        JankTracker::dumpBuffer(buffer.get(), buffer.size(), fd);
    }
}

該方法作了一些轉換,就調用了 JankTracker的dumpBuffer(buffer.get(), buffer.size(), fd),對應的文件是/frameworks/base/libs/hwui/JankTracker.cpp。hwui意思是hardware ui,跟圖像渲染的硬件加速相關,而jank tracker的意思是卡頓追蹤,一看名字就知道咱們找對了。相關的方法以下:

void JankTracker::dumpBuffer(const void* buffer, size_t bufsize, int fd) {
    if (bufsize < sizeof(ProfileData)) {
        return;
    }
    const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer);
    dumpData(data, fd);
}

void JankTracker::dumpData(const ProfileData* data, int fd) {
    dprintf(fd, "\nTotal frames rendered: %u", data->totalFrameCount);
    dprintf(fd, "\nJanky frames: %u (%.2f%%)", data->jankFrameCount,
            (float) data->jankFrameCount / (float) data->totalFrameCount * 100.0f);
    dprintf(fd, "\n90th percentile: %ums", findPercentile(data, 90));
    dprintf(fd, "\n95th percentile: %ums", findPercentile(data, 95));
    dprintf(fd, "\n99th percentile: %ums", findPercentile(data, 99));

    for (int i = 0; i < NUM_BUCKETS; i++) {
        dprintf(fd, "\nNumber %s: %u", JANK_TYPE_NAMES[i], data->jankTypeCounts[i]);
    }
    dprintf(fd, "\n");
}

能夠看到,dumpData最終完成的最後的輸出,相關的結構體有JANK_TYPE_NAMESProfileData。JANK_TYPE_NAMES其實就是一個字符常量數組,裏面存了卡頓的類型,定義在文件的前頭:

static const char* JANK_TYPE_NAMES[] = {
        "Missed Vsync",
        "High input latency",
        "Slow UI thread",
        "Slow bitmap uploads",
        "Slow issue draw commands",
};

ProfileData則是一個定義在JankTracer.h中的結構體:

struct ProfileData {
    uint32_t jankTypeCounts [NUM_BUCKETS];
    uint32_t frameCounts [57] ;

    uint32_t totalFrameCount;
    uint32_t jankFrameCount;
};

看起來也沒啥特別的,如今的關鍵就是data中的數據是何時誰填進去的。

5.渲染統計信息的收集

看到如今,是否是有點暈了,哈哈,還記得data是從哪裏來的嗎?首先,data來自於buffer,它是在dumpBuffer中由buffer指針強制轉換而來,而buffer則是層層傳下來的,最終的源頭是requestBufferForProcess方法分配的buffer!

如此一來,內存分配的前因後果已經解決了。鍋已經造好了,那麼是誰往裏面放東西的呢?那就取決於誰調用了GraphicsStatsService的requestBufferForProcess。注意到requestBufferForProcess是一個由AIDL定義的接口,調用者確定使用了跨進程的方法調用了它,那麼去哪裏找這些調用呢。別忘了,在第3小節中,咱們分析到ThreadedRenderer的initGraphicsStats調用了requestBufferForProcess,而後經過本地方法nSetProcessStatsBuffer(renderProxy, pfd.getFd()) 將渲染的代理類與文件描述符關聯。根據對代理模式的最基本的瞭解,真正進行渲染工做的應該是這個代理類RenderProxy,因此咱們將追蹤的目標轉移到它身上。

1)SetProcessStatsBuffer的native流程

先讓咱們看看nSetProcessStatsBuffer(),其定義在frameworks/base/libs/hwui/renderthread/RenderProxy.cpp中,

CREATE_BRIDGE2(setProcessStatsBuffer, RenderThread* thread, int fd) {
    args->thread->jankTracker().switchStorageToAshmem(args->fd);
    close(args->fd);
    return nullptr;
}

void RenderProxy::setProcessStatsBuffer(int fd) {
    SETUP_TASK(setProcessStatsBuffer);
    args->thread = &mRenderThread;
    args->fd = dup(fd);
    post(task);
}

CREATE_BRIDGE2和SETUP_TASK都是宏定義,挺複雜的,這裏就不貼這兩個宏定義的代碼了,這裏直接給出展開後的結構,有興趣的朋友能夠去本身研究研究。

typedef struct { 
       RenderThread* thread,
       int fd
} setProcessStatsBufferArgs; 

static void* Bridge_setProcessStatsBuffer(setProcessStatsBufferArgs* args){
    args->thread->jankTracker().switchStorageToAshmem(args->fd);
    close(args->fd);
    return nullptr;
}

void RenderProxy::setProcessStatsBuffer(int fd) {
    MethodInvokeRenderTask* task = new MethodInvokeRenderTask(
         (RunnableMethod) Bridge_setProcessStatsBuffer); 
    setProcessStatsBufferArgs *args = (setProcessStatsBufferArgs *) task->payload();
    args->thread = &mRenderThread;
    args->fd = dup(fd);
    post(task);
}

能夠看到,第一個宏定義了一個結構setProcessStatsBufferArgs和一個方法Bridge_setProcessStatsBuffer,第二個宏定義了nSetProcessStatsBuffer()函數自己,當nSetProcessStatsBuffer()被調用時,就會新建一個MethodInvokeRenderTask,並把Bridge_setProcessStatsBuffer做爲回調函數,而後爲thread賦值&mRenderThread,爲fd複製一個fd,最後提交新建的任務。咱們來看看MethodInvokeRenderTask的定義,frameworks/base/libs/hwui/renderthread/RenderTask.h中:

typedef void* (*RunnableMethod)(void* data);

class MethodInvokeRenderTask : public RenderTask {
public:
    MethodInvokeRenderTask(RunnableMethod method)
        : mMethod(method), mReturnPtr(nullptr) {}

    void* payload() { return mData; }
    void setReturnPtr(void** retptr) { mReturnPtr = retptr; }

    virtual void run() override {
        void* retval = mMethod(mData);
        if (mReturnPtr) {
            *mReturnPtr = retval;
        }
        // Commit suicide
        delete this;
    }
private:
    RunnableMethod mMethod;
    char mData[METHOD_INVOKE_PAYLOAD_SIZE];
    void** mReturnPtr;
};

MethodInvokeRenderTask繼承於RenderTask,在構造函數中,用method初始化mMethod,用nullptr初始化mReturnPtr。函數payload()直接返回mData,實際上是一個字符數組mData[METHOD_INVOKE_PAYLOAD_SIZE]。任務被執行的時候,就是執行虛函數run(),注意到有一句mMethod(mData),這裏調用傳進來的方法mMethod,其實就是Bridge_setProcessStatsBuffer,實參是mData,而mData被轉換成了(setProcessStatsBufferArgs *),因而成功調用了Bridge_setProcessStatsBuffer(setProcessStatsBufferArgs *),最後的功能由args->thread->jankTracker(). switchStorageToAshmem(args->fd)完成。

首先咱們看下switchStorageToAshmem這個函數,位於frameworks/base/libs/hwui/JankTracker.cpp:

void JankTracker::switchStorageToAshmem(int ashmemfd) {
   ...

    ProfileData* newData = reinterpret_cast<ProfileData*>(
            mmap(NULL, sizeof(ProfileData), PROT_READ | PROT_WRITE,
            MAP_SHARED, ashmemfd, 0));
    if (newData == MAP_FAILED) {
        int err = errno;
        ALOGW("Failed to move profile data to ashmem fd %d, error = %d",
                ashmemfd, err);
        return;
    }

    ...

    if (newData->totalFrameCount > (1 << 24)) {
        divider = 4;
    }
    for (size_t i = 0; i <(sizeof(mData->jankTypeCounts) / sizeof(mData->jankTypeCounts[0])); i++) {
        newData->jankTypeCounts[i] >>= divider;
        newData->jankTypeCounts[i] += mData->jankTypeCounts[i];
    }
    for (size_t i = 0; i <(sizeof(mData->frameCounts) / sizeof(mData->frameCounts[0])); i++) {
        newData->frameCounts[i] >>= divider;
        newData->frameCounts[i] += mData->frameCounts[i];
    }
    newData->jankFrameCount >>= divider;
    newData->jankFrameCount += mData->jankFrameCount;
    newData->totalFrameCount >>= divider;
    newData->totalFrameCount += mData->totalFrameCount;
    if (newData->statStartTime > mData->statStartTime
            || newData->statStartTime == 0){
        newData->statStartTime = mData->statStartTime;
    }

    freeData();
    mData = newData;
    mIsMapped = true;
}

這函數主要是完成線程私有內存在共享內存的映射。首先使用 mmap(NULL, sizeof(ProfileData), PROT_READ | PROT_WRITE,MAP_SHARED, ashmemfd, 0)申請了一個共享newData,而後把mData的數據映射進去,最後把mData指向這塊內存完成映射,並mIsMapped置true。至此,爲進程新建儲存渲染統計信息的buffer的流程就完全走完啦。

2)jankTracker的初始化過程

而args->thread的值是mRenderThread,其實就是被代理的RenderThread,調用jankTracker得到保存其保存的對jankTracker的引用。那麼這個jankTraker是在哪裏被初始化的呢?答案就在/frameworks/base/libs/hwui/renderthread/RenderThread.cpp中:

void RenderThread::initThreadLocals() {
    sp<IBinder> dtoken(SurfaceComposerClient::getBuiltInDisplay(
            ISurfaceComposer::eDisplayIdMain));
    status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &mDisplayInfo);
    LOG_ALWAYS_FATAL_IF(status, "Failed to get display info\n");
    nsecs_t frameIntervalNanos = static_cast<nsecs_t>(1000000000 / mDisplayInfo.fps);
    mTimeLord.setFrameInterval(frameIntervalNanos);
    initializeDisplayEventReceiver();
    mEglManager = new EglManager(*this);
    mRenderState = new RenderState(*this);
    mJankTracker = new JankTracker(frameIntervalNanos);
}

這個函數首先取得屏幕的顯示參數mDisplayInfo,而後利用mDisplayInfo.fps去初始化JankTracker。

JankTracker的代碼位於frameworks/base/libs/hwui/JankTracker.cpp:

JankTracker::JankTracker(nsecs_t frameIntervalNanos) {
    // By default this will use malloc memory. It may be moved later to ashmem
    // if there is shared space for it and a request comes in to do that.
    mData = new ProfileData;
    reset();
    setFrameInterval(frameIntervalNanos);
}
...
void JankTracker::setFrameInterval(nsecs_t frameInterval) {
    mFrameInterval = frameInterval;
    mThresholds[kMissedVsync] = 1;
    /*
     * Due to interpolation and sample rate differences between the touch
     * panel and the display (example, 85hz touch panel driving a 60hz display)
     * we call high latency 1.5 * frameinterval
     *
     * NOTE: Be careful when tuning this! A theoretical 1,000hz touch panel
     * on a 60hz display will show kOldestInputEvent - kIntendedVsync of being 15ms
     * Thus this must always be larger than frameInterval, or it will fail
     */
    mThresholds[kHighInputLatency] = static_cast<int64_t>(1.5 * frameInterval);

    // Note that these do not add up to 1. This is intentional. It's to deal
    // with variance in values, and should be sort of an upper-bound on what
    // is reasonable to expect.
    mThresholds[kSlowUI] = static_cast<int64_t>(.5 * frameInterval);
    mThresholds[kSlowSync] = static_cast<int64_t>(.2 * frameInterval);
    mThresholds[kSlowRT] = static_cast<int64_t>(.75 * frameInterval);

}

構造函數首先new 一個ProfileData,而後將其重置,最後把參數傳遞給setFrameInterval並調用。

而在setFrameInterval中咱們彷佛看到了一些熟悉的東西,五個閾值:kMissedVsync,kHighInputLatency,kSlowUI,kSlowSync和kSlowRT,kMissedVsync固定爲1,而其餘幾個分別是frameInterval的1.5倍,0.5倍,0.2倍,0.75倍,因爲傳進來的frameIntervalNanos的值爲1000000000 / mDisplayInfo.fps,單位是納秒,因此mDisplayInfo.fps的值通常是60,因此frameInterval的值通常爲16.6ms,後面的幾個閾值分別是kHighInputLatency = 25ms,kSlowUI = 8.3ms,kSlowSync = 3.3ms和kSlowRT = 12.5ms。這幾個值應該是用來衡量一個幀不一樣階段的渲染時間性能,具體有什麼意義咱們後面再看。

3)對ProfileData(mData)的操做

從前面分析中,咱們能夠知道,mData就是存放渲染統計信息的數據結構,下面咱們看看JankTracker中對mData的操做。操做主要有三個:addFrame、reset和freeData,其中reset和freeData分別用於重置和清除數據,比較重要的是addFrame,用於添加一幀的渲染信息:

void JankTracker::addFrame(const FrameInfo& frame) {
    mData->totalFrameCount++;
    using namespace FrameInfoIndex;
    // Fast-path for jank-free frames
    int64_t totalDuration = frame[kFrameCompleted] - frame[kIntendedVsync];
    uint32_t framebucket = frameCountIndexForFrameTime(
            totalDuration,  (sizeof(mData->frameCounts) / sizeof(mData->frameCounts[0])) );
    //keep the fast path as fast as possible
    if (CC_LIKELY(totalDuration < mFrameInterval)) {
        mData->frameCounts[framebucket]++;
        return;
    }

    //exempt this frame, so drop it
    if (frame[kFlags] & EXEMPT_FRAMES_FLAGS) {
        return;
    }

    mData->frameCounts[framebucket]++;
    mData->jankFrameCount++;

    for (int i = 0; i < NUM_BUCKETS; i++) {
        int64_t delta = frame[COMPARISONS[i].end] - frame[COMPARISONS[i].start];
        if (delta >= mThresholds[i] && delta < IGNORE_EXCEEDING) {
            mData->jankTypeCounts[i]++;
        }
    }
}

首先將總幀數totalFrameCount加1,而後計算這一幀的渲染時間totalDuration,值等於結束時間kFrameCompleted-發送Vsync信號的時間kIntendedVsync。而後若是totalDuration的時間小於mFrameInterval(16.6ms),那麼這一幀就是不卡頓的,直接返回。

第二個if,判斷這個幀是不是「豁免」的,若是是也直接返回。那麼什麼幀是「豁免」的呢?先看看這個常量EXEMPT_FRAMES_FLAGS,

static const int64_t EXEMPT_FRAMES_FLAGS
        = FrameInfoFlags::kWindowLayoutChanged
        | FrameInfoFlags::kSurfaceCanvas;

能夠看到這個EXEMPT_FRAMES_FLAGS等於kWindowLayoutChanged|kSurfaceCanvas,也就說若是一個幀是跟kWindowLayoutChanged有關的或者是用一個專門的SurfaceCanvas來繪製的,那麼這個幀不在統計的範圍以內。在這個定義以前有一大段註釋,大概意思是:有一些幀是不作卡頓統計的,好比那些第一次繪製的,用戶以爲慢也是正常的,能夠被動畫或者其餘手段掩蓋的幀,還有那些繪製在surface上面的幀。

若是前面的兩個if都沒有返回,那麼代表這個幀是卡頓的,開始作卡頓統計。在最下面的那個for循環裏面,開始遍歷每一個bucket,就是上面所說的那五種卡頓類型,而後就算每種類型的所消耗的時間delta=COMPARISONS[i].end - COMPARISONS[i].start,若是這個時間大於上面所說的閾值mThresholds,那麼將對應的卡頓類型數+1。

4)卡頓類型的具體檢測區間

COMPARISONS是一個數組,定義以下:

static const char* JANK_TYPE_NAMES[] = {
        "Missed Vsync",
        "High input latency",
        "Slow UI thread",
        "Slow bitmap uploads",
        "Slow draw",
};

struct Comparison {
    FrameInfoIndexEnum start;
    FrameInfoIndexEnum end;
};

static const Comparison COMPARISONS[] = {
        {FrameInfoIndex::kIntendedVsync, FrameInfoIndex::kVsync},
        {FrameInfoIndex::kOldestInputEvent, FrameInfoIndex::kVsync},
        {FrameInfoIndex::kVsync, FrameInfoIndex::kSyncStart},
        {FrameInfoIndex::kSyncStart, FrameInfoIndex::kIssueDrawCommandsStart},
        {FrameInfoIndex::kIssueDrawCommandsStart, FrameInfoIndex::kFrameCompleted},
};

從中,咱們能夠看到,Missed Vsync和High input latency時間區間是有重合的,他們開始的時間不同,可是結束的時間都是kVsync,後面三個bukect的時間都是首尾相連的,到這裏這個五個bukect的時間區間已經很明確了:

Vsync階段:KIntendedVsync 到 KVsync

input階段:KOldestInputEvent 到 KVsync

UI thread階段:KVsync 到 KSyncStart

bitmap uploads階段:KSyncStart 到 KIssueDrawCommandsStart

draw階段:KIssueDrawCommandsStart 到 KFrameCompleted

6.總結

那麼到如今,具體填進去的操做已經看完了,那麼系統在哪裏調用了這個addFrame呢?而後這個mData有是怎麼交給dump環節來輸出的呢?

然咱們先來回答一下第二個問題。其實總結一下前面的就能夠知道了,綜述一下:Java層ThreadedRenderer在其初始化的過程當中調用了GraphicsStatsService.requestBufferForProcess爲渲染進程分配了放置存儲統計數據的匿名共享內存buffer,返回一個fd,並將其放在mActive統一管理,供java層之後調用;而native層是用RenderProxy來進行任務的代理,RenderProxy又調用了JankTracker.switchStorageToAshmem()來真正完成這一任務,生成了一塊ProfileData類型的內存,由mData持有引用,由JankTracker.addFrame()負責往其中填寫數據。當咱們須要dump的時候,GraphicsStatsService就會取出全部mActive全部保存好的buffer,在經過JNI交由JankTracker自己的dumpData來輸出,兜了一大圈,又轉回來了。能夠發如今在整個過程當中,JankTracker纔是真正核心的類。

那麼整個過程就剩下一個疑問點啦,那就是addFrame是在什麼地方被調用的。這個涉及到view的渲染過程,我會在下一篇博客中解剖。

最後說在後面的,這以上內容都是我我的的理解,我也是第一次看這方面的內容,不少地方也不理解,有不少都是猜的,因此這篇博客更多的是個人代碼閱讀筆記,做爲一個實習生,個人水平頗有限,錯漏的地方確定有不少,歡迎批評指出。

相關文章
相關標籤/搜索