美文网首页Android Internals
GraphicsStatsService之1-dump数据

GraphicsStatsService之1-dump数据

作者: SyntaxWarnning | 来源:发表于2018-07-13 00:54 被阅读0次

    文中所有代码基于Android8.0

    不了解dumpsys,可以先看Android dumpsys 实现

    1.执行dump

    测试android性能,其中帧率很重要,执行adb shell dumpsys graphicsstats,能得到类似如下结果:

    ...
    //包名
    Package: android
    //系统开机多久后开始统计的
    Stats since: 253812513812ns
    //总共绘制的帧数
    Total frames rendered: 479
    //卡顿帧数,16ms没绘制完的
    Janky frames: 25(5.23%)
    // 50% 90% 95% 99% 的帧数是多长时间绘制完成的
    50th percentile: 5ms
    90th percentile: 23ms
    95th percentile: 35ms
    99th percentile: 43ms
    
    // 丢失的Vsync信号
    Number Missed Vsync: 12
    //高输入导致的
    Number High input latency: 0
    //ui线程慢
    Number Slow UI thread: 7
    //上传绘制bitmap
    Number Slow bitmap uploads: 3
    //绘制命令异常
    Number Slow issue draw commands: 15
    
    // 这是每个时间对应的绘制帧数
    HISTOGRAM: 5ms=4620 6ms=622 7ms=328 8ms=198 9ms=332 ...非常多... 4150ms=0 4200ms=0 4250ms=0 4300ms=0 4350ms=0 4400ms=0 4450ms=0 4500ms=0 4550ms=0 4600ms=0 4650ms=0 4700ms=0 4750ms=0 4800ms=0 4850ms=0 4900ms=0 4950ms=0
    ...
    

    2 执行流程

    主要用到的类:
    GraphicsStatsService.java
    com_android_server_GraphicsStatsService.cpp
    GraphicsStatsService.cpp

    Android dumpsys 实现一文提到如何service是如何dump的,graphicsstats这个对应的服务是GraphicsStatsService,看一下它是如何将数据dump的。
    所有的系统服务从binder继承的dump接口,在GraphicsStatsService.java中:

    @Override
        protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
            if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, fout)) return;
            //1 解析参数
            boolean dumpProto = false;
            for (String str : args) {
                if ("--proto".equals(str)) {
                    dumpProto = true;
                    break;
                }
            }
            //2 收集buffers
            ArrayList<HistoricalBuffer> buffers;
            synchronized (mLock) {
                buffers = new ArrayList<>(mActive.size());
                for (int i = 0; i < mActive.size(); i++) {
                    try {
                        buffers.add(new HistoricalBuffer(mActive.get(i)));
                    } catch (IOException ex) {
                        // Ignore
                    }
                }
            }
            //3 创建dump对象
            long dump = nCreateDump(fd.getInt$(), dumpProto);
            try {
                synchronized (mFileAccessLock) {
                   //4 dump数据
                    HashSet<File> skipList = dumpActiveLocked(dump, buffers);
                    buffers.clear();
                    dumpHistoricalLocked(dump, skipList);
                }
            } finally {
                // 5 完成dump
                nFinishDump(dump);
            }
        }
    

    2.1 解析参数,当 dump graphicsstats --proto 时,加了这个参数会按proto格式打印,反正人没法认出来...
    2.2 收集buffer,这步比较重要,看两个数据结构ActiveBuffer 和 HistoricalBuffer

     private final class ActiveBuffer implements DeathRecipient {
            ...
            MemoryFile mProcessBuffer;
    
            ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
                    throws RemoteException, IOException {
                ...
                // 创建 MemoryFile
                mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE);
                mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE);
            }
            ....
             
             
        }
    
      private final class HistoricalBuffer {
            final BufferInfo mInfo;
            final byte[] mData = new byte[ASHMEM_SIZE];
            HistoricalBuffer(ActiveBuffer active) throws IOException {
                mInfo = active.mInfo;
                mInfo.endTime = System.currentTimeMillis();
                //读取数据
                active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE);
            }
        }
    
    

    ActiveBuffer在创建时,new了一个MemoryFile,也就是共享内存,并且初始化为0值。在绘制时会将数据填充。当收集buffer时,在HistoricalBuffer里,将数据读到了它的成员mData中,其大小为ASHMEM_SIZE,是通过一个native方法拿到的:

    # com_android_server_GraphicsStatsService.cpp
    
    static jint getAshmemSize(JNIEnv*, jobject) {
        return sizeof(ProfileData);
    }
    

    是一个名为ProfileData的结构体的大小。(下一篇聊这个结构体,它关乎数据的来源)这样数据就收集完了。

    2.3 创建dump,在底层做了什么呢?

    long dump = nCreateDump(fd.getInt$(), dumpProto);
    

    fd.getInt$(),拿到要写入的fd。
    dumpProto 默认是false

    static jlong createDump(JNIEnv*, jobject, jint fd, jboolean isProto) {
        GraphicsStatsService::Dump* dump = GraphicsStatsService::createDump(fd, isProto
                ? GraphicsStatsService::DumpType::Protobuf : GraphicsStatsService::DumpType::Text);
        return reinterpret_cast<jlong>(dump);
    }
    

    原来是new了一个GrahicsStatsService.cpp里的一个Dump对象,然后返回了它的指针。Dump类如下:

    class GraphicsStatsService::Dump {
    public:
        Dump(int outFd, DumpType type) : mFd(outFd), mType(type) {}
        int fd() { return mFd; }
        DumpType type() { return mType; }
        service::GraphicsStatsServiceDumpProto& proto() { return mProto; }
    private:
        int mFd;
        DumpType mType;
        service::GraphicsStatsServiceDumpProto mProto;
    };
    

    这个Dump对象主要作用是在底层保存了要写入的fd 。
    2.4 上一步得到了一个底层的指针,接下来就是打印了:

     private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) {
            HashSet<File> skipFiles = new HashSet<>(buffers.size());
            for (int i = 0; i < buffers.size(); i++) {
                HistoricalBuffer buffer = buffers.get(i);
                File path = pathForApp(buffer.mInfo);
                skipFiles.add(path);
                nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName,
                        buffer.mInfo.versionCode,  buffer.mInfo.startTime, buffer.mInfo.endTime,
                        buffer.mData);
            }
            return skipFiles;
        }
    

    主要是nAddToDump这个方法:

    // 去除了一些打印语句
    # com_android_server_GraphicsStatsService.cpp
    static void addToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath, jstring jpackage,
            jint versionCode, jlong startTime, jlong endTime, jbyteArray jdata) {
        std::string path;
        const ProfileData* data = nullptr;
        
        ScopedByteArrayRO buffer{env};
        if (jdata != nullptr) {
            buffer.reset(jdata);
            // 1 转换成ProfileData结构
            data = reinterpret_cast<const ProfileData*>(buffer.get());
        }
        if (jpath != nullptr) {
            ScopedUtfChars pathChars(env, jpath);
            path.assign(pathChars.c_str(), pathChars.size());
        }
        ScopedUtfChars packageChars(env, jpackage);
        GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
    
        const std::string package(packageChars.c_str(), packageChars.size());
        // 2 调用GraphicsStatsService.cpp里的addToDump
        GraphicsStatsService::addToDump(dump, path, package, versionCode, startTime, endTime, data);
    }
    

    2.4.1 刚才数据是按ProfileData这个结构体大小读的,现在将数据转换成这个结构。
    2.4.2 调用native类的addToDump方法:

    # GraphicsStatsService.cpp
    void GraphicsStatsService::addToDump(Dump* dump, const std::string& path, const std::string& package,
            int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
        //1 从指定路径读数据
        service::GraphicsStatsProto statsProto;
        if (!path.empty() && !parseFromFile(path, &statsProto)) {
            statsProto.Clear();
        }
        // 2 合并两份数据
        if (data && !mergeProfileDataIntoProto(
                &statsProto, package, versionCode, startTime, endTime, data)) {
            return;
        }
        if (!statsProto.IsInitialized()) {
            ALOGW("Failed to load profile data from path '%s' and data %p",
                    path.empty() ? "<empty>" : path.c_str(), data);
            return;
        }
    
        if (dump->type() == DumpType::Protobuf) {
            dump->proto().add_stats()->CopyFrom(statsProto);
        } else {
             // 3 打印数据,不需要protobuf格式
            dumpAsTextToFd(&statsProto, dump->fd());
        }
    }
    

    2.4.2.a: 从指定路径读数据,为何还要读呢,不是从共享内存取的吗?
    实际上数据在app死亡或者定时器到了时间,会调整数据,会将数据保存到data/system下的。下一篇讨论数据来源。

    
    bool GraphicsStatsService::parseFromFile(const std::string& path, service::GraphicsStatsProto* output) {
        ...
        void* addr = mmap(nullptr, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
        ...
        void* data = reinterpret_cast<uint8_t*>(addr) + sHeaderSize;
        int dataSize = sb.st_size - sHeaderSize;
        io::ArrayInputStream input{data, dataSize};
        bool success = output->ParseFromZeroCopyStream(&input);
        return success;
    }
    

    核心是用protobuf的接口,将数据专成GraphicsStatsProto结构。

    2.4.2.b: merge数据,将两份数据合并一下,大多数就是持久化在文件里的,跟内存里的做加法。
    2.4.2.c: dump数据:

    
    void dumpAsTextToFd(service::GraphicsStatsProto* proto, int fd) {
        // This isn't a full validation, just enough that we can deref at will
        if (proto->package_name().empty() || !proto->has_summary()) {
            ALOGW("Skipping dump, invalid package_name() '%s' or summary %d",
                    proto->package_name().c_str(), proto->has_summary());
            return;
        }
        dprintf(fd, "\nPackage: %s", proto->package_name().c_str());
        dprintf(fd, "\nVersion: %d", proto->version_code());
        dprintf(fd, "\nStats since: %lldns", proto->stats_start());
        dprintf(fd, "\nStats end: %lldns", proto->stats_end());
        auto summary = proto->summary();
        dprintf(fd, "\nTotal frames rendered: %d", summary.total_frames());
        dprintf(fd, "\nJanky frames: %d (%.2f%%)", summary.janky_frames(),
                (float) summary.janky_frames() / (float) summary.total_frames() * 100.0f);
        dprintf(fd, "\n50th percentile: %dms", findPercentile(proto, 50));
        dprintf(fd, "\n90th percentile: %dms", findPercentile(proto, 90));
        dprintf(fd, "\n95th percentile: %dms", findPercentile(proto, 95));
        dprintf(fd, "\n99th percentile: %dms", findPercentile(proto, 99));
        dprintf(fd, "\nNumber Missed Vsync: %d", summary.missed_vsync_count());
        dprintf(fd, "\nNumber High input latency: %d", summary.high_input_latency_count());
        dprintf(fd, "\nNumber Slow UI thread: %d", summary.slow_ui_thread_count());
        dprintf(fd, "\nNumber Slow bitmap uploads: %d", summary.slow_bitmap_upload_count());
        dprintf(fd, "\nNumber Slow issue draw commands: %d", summary.slow_draw_count());
        dprintf(fd, "\nHISTOGRAM:");
        for (const auto& it : proto->histogram()) {
            dprintf(fd, " %dms=%d", it.render_millis(), it.frame_count());
        }
        dprintf(fd, "\n");
    }
    

    至此,我们看到,将数据写入到了从java传过来的fd中,也是在Android dumpsys 实现一文中提到的,pipe创建的管道的写入端。这样结合dumpsys实现,看到了数据最终输出到了终端。

    下一篇讨论数据来源。

    相关文章

      网友评论

        本文标题:GraphicsStatsService之1-dump数据

        本文链接:https://www.haomeiwen.com/subject/kafysftx.html