Android 性能优化之 GraphicsStatsService

1.概述

GraphicsStatsService是Android M(6.0)以后Google加入的用于收集汇总Android系统的渲染剖面数据(profile data),主要途径是通过允许渲染线程请求匿名共享存储缓冲(ashmem buffer)来存放它们的统计信息来实现的。这篇文章旨在分析GraphicsStatsService的工作流程和这些统计信息的来龙去脉。

首先来看下GraphicsStatsService都收集了哪些信息。通过adb shell dumpsys graphicsstats 可以输出GraphicsStatsService收集的信息,以下是在小米手机上执行该命令时输出的信息:

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: 5408

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: 10

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

………..

可以看到输出很多应用的渲染信息,以包名作为区分。其中Stats since表示该应用的统计信息是从系统开机多长时间(纳秒)后开始统计的Total frames表示一共绘制了多少帧,Janky frames表示有多少帧是卡顿的,90th percentile、95th percentile、99th percentile分别表示90%、95%、99%的帧是在多少毫秒内完成的;最后的五个指标表示卡顿的具体原因及卡顿的帧数(一个帧可能有多个卡顿的原因),具体的解释可以看第5章的第4小结。这些信息可以帮助应用开发者分析其应用的卡顿情况,也可以帮助系统开发了解整个系统的性能情况。

那么这GraphicsStatsService服务运作机制是如何的呢?这些统计数据都是怎么收集的?下面让我们一步步来探索,首先看一下GraphicsStatsService都长啥样。

2.GraphicsStatsService类的解析

GraphicsStatsService 类文件位于frameworks/base/services/core/java/com/android/server /GraphicsStatsService.java,系统很多核心的服务都位于该目录下。

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的渲染过程,我会在下一篇博客中解剖。

最后说在后面的,这以上内容都是我个人的理解,我也是第一次看这方面的内容,很多地方也不理解,有很多都是猜的,所以这篇博客更多的是我的代码阅读笔记,作为一个实习生,我的水平很有限,错漏的地方肯定有很多,欢迎批评指出。

    原文作者:Android
    原文地址: https://juejin.im/entry/58c20f69128fe1006b26f40f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞