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_NAMES和ProfileData。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的渲染过程,我会在下一篇博客中解剖。
最后说在后面的,这以上内容都是我个人的理解,我也是第一次看这方面的内容,很多地方也不理解,有很多都是猜的,所以这篇博客更多的是我的代码阅读笔记,作为一个实习生,我的水平很有限,错漏的地方肯定有很多,欢迎批评指出。