当前位置: 首页 > news >正文

GraphicsStatsService之1-dump数据的实现

文中所有代码基于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实现,看到了数据最终输出到了终端。

下一篇讨论数据来源。

相关文章:

  • Nginx(转)
  • react-create-app
  • 好用的Vue状态管理模式:浅谈Vuet在实际应用中解决的问题
  • Android 解决 View 的滑动冲突
  • RabbitMQ-Java版本生产与消费
  • Ajax学习(一)
  • window对象
  • saltstack常用远程命令
  • vuex,vue问题汇集(一)
  • ERROR   OGG-01161 Bad column index (15) specified for table
  • HNUSTOJ-1520 压缩编码
  • java json与map互相转换(一)
  • 『TensotFlow』RNN中文文本_上
  • 高并发网络编程之epoll详解(转载)
  • AdTime:DMC多层面助力企业咨询
  • 时间复杂度分析经典问题——最大子序列和
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • 【RocksDB】TransactionDB源码分析
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • crontab执行失败的多种原因
  • KMP算法及优化
  • spring学习第二天
  • Swoft 源码剖析 - 代码自动更新机制
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • WinRAR存在严重的安全漏洞影响5亿用户
  • 程序员该如何有效的找工作?
  • 给Prometheus造假数据的方法
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 排序算法之--选择排序
  • 使用Swoole加速Laravel(正式环境中)
  • 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)
  • 数据可视化之 Sankey 桑基图的实现
  • 通过几道题目学习二叉搜索树
  • 微信开源mars源码分析1—上层samples分析
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • hi-nginx-1.3.4编译安装
  • Nginx惊现漏洞 百万网站面临“拖库”风险
  • ​2021半年盘点,不想你错过的重磅新书
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • ​直流电和交流电有什么区别为什么这个时候又要变成直流电呢?交流转换到直流(整流器)直流变交流(逆变器)​
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • #ifdef 的技巧用法
  • #Spring-boot高级
  • $.ajax,axios,fetch三种ajax请求的区别
  • (二)fiber的基本认识
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (原創) 系統分析和系統設計有什麼差別? (OO)
  • (转)Google的Objective-C编码规范
  • *p++,*(p++),*++p,(*p)++区别?
  • .Net MVC4 上传大文件,并保存表单
  • .Net下的签名与混淆
  • /etc/fstab和/etc/mtab的区别