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实现,看到了数据最终输出到了终端。

下一篇讨论数据来源。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容