文档章节

Android性能优化之GraphicsStatsService(1)

西皇小明
 西皇小明
发布于 2017/01/17 14:45
字数 5267
阅读 880
收藏 1

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

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

© 著作权归作者所有

共有 人打赏支持
西皇小明
粉丝 5
博文 40
码字总数 20601
作品 0
海淀
程序员
私信 提问
加载中

评论(2)

邵翔宇
邵翔宇
前辈 ,您是miui的嘛?我也是miui的。我的微信是shaoxy1992 方便加下吗?我自己用py写了一个评测流畅度的工具 这个工具每隔1.2秒执行一下adb shell dumpsys gfxinfo com.miui.gallery framestats 同时uiautomator操作app 并且对结果去重 最后生成html柱状图展示 9个阶段的帧耗时 可以展示几千帧不卡.希望和你交流下.
邵翔宇
邵翔宇
前辈 ,您是miui的嘛?我也是miui的。我的微信是shaoxy1992 方便加下吗?我自己用py写了一个评测流畅度的工具 这个工具每隔1.2秒执行一下adb shell dumpsys gfxinfo com.miui.gallery framestats 同时uiautomator操作app 并且对结果去重 最后生成html柱状图展示 9个阶段的帧耗时 可以展示几千帧不卡.希望和你交流下.
高手问答第 150 期 — Android 应用性能优化

OSCHINA 本期高手问答(2017 年 4 月 25 日 — 5 月 1 日)我们请来了 @yuchengluo (罗彧成)为大家解答 Android 应用性能优化相关的问题。 @yuchengluo ,罗彧成。腾讯音乐 Android 开发总...

局长
2017/04/24
3.6K
29
fastjson 1.1.47-android 发布,大幅提升性能

Android环境下性能大幅度提升,减少内存占用,jar包大小不足200k。 1. 性能优化。 1.1.47-android针对android做了很多性能优化,性能优化包括首次序列化/反序列化,在android环境,序列化的次...

wenshao
2016/04/04
3.5K
16
Android性能优化:这是一份详细的布局优化 指南(含、、)

前言 在 开发中,性能优化策略十分重要 本文主要讲解性能优化中的布局优化,希望你们会喜欢。 目录 /** 实例说明:在上述例子,在布局B中 通过标签引用布局C 此时:布局层级为 = RelativeLa...

Carson_Ho
2018/05/14
0
0
Android性能调优工具TraceView介绍

本文主要介绍Android性能调优工具TraceView的使用及通过其确定性能点。 Android自带的TraceView可以方便的查看线程的执行情况,某个方法执行时间、调用次数、在总体中的占比等,从而定位性能...

Trinea
2013/04/09
744
0
Android性能优化:那些不可忽略的绘制优化

前言 在 开发中,性能优化策略十分重要 本文主要讲解性能优化中的绘制优化,希望你们会喜欢。 目录 // 方式2:在 BaseActivity 的 onCreate() 方法中使用下面的代码移除 优化方案2:移除 控件...

Carson_Ho
2018/05/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

ShxViewer_SHX字体查看

ShxViewe 是一款非常实用的SHX字型浏览软件。从CAD里面的字体浏览软件分离出来,帮助我们预览shx字体。 程序长这个样子: 分别打开txt.shx、hztxt.shx、ltypeshp.shx这几个形文件,可以了解一...

一个小妞
10分钟前
0
0
Jenkins的初步使用

Jenkins真是个宝藏软件,今天大概安装使用了一下,感觉还有好多维度可以探索。 1)安装:在Windows上使用的,在https://jenkins.io/download/下载Windows安装包,解压后是一个msi文件,默认安...

莫在全
22分钟前
0
0
技术复习-分布式事务

一、分布式事务解决方案 1.两阶段提交 two phase commit 角色分为协调者、参与者。协调者负责协调所有的参与者。 第一阶段 prepare 协调者发送prepare请求,参与者锁定资源之后返回ready或者...

Lubby
32分钟前
1
0
jenkins安装

https://my.oschina.net/u/593517/blog/1797968 jenkins 安装 https://my.oschina.net/u/593517/blog/3028175 GIT 安装 https://my.oschina.net/u/593517/blog/3028179 maven 安装 插件安装 ......

Gm_ning
42分钟前
2
0
小言服务端解决方案-监控

框架保证方向,整体包容细节 为保证服务端运行平稳正常,owner应使得系统应保有相应的监控:系统监控,业务监控。而服务运行的平稳高效是否有保障跟监控粒度又成直接的正比关系。本文仅针对开...

重城重楼
54分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部