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