Protobuf 3.9.1 redefines google::protobuf::uint64 from unsigned long long to uint64_t, which is sometimes unsigned long and sometimes unsigned long long. Use PRIu64 to print it, and add an implementation of ProtoOutputStream::write for long. Bug: 117607748 Test: m checkbuild Change-Id: Ib2d3f4e17857f8ccbbe342ce6678e76b591df510 Merged-In: Ib2d3f4e17857f8ccbbe342ce6678e76b591df510
373 lines
14 KiB
C++
373 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "GraphicsStatsService.h"
|
|
|
|
#include "JankTracker.h"
|
|
#include "protos/graphicsstats.pb.h"
|
|
|
|
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
|
|
#include <log/log.h>
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <inttypes.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
namespace android {
|
|
namespace uirenderer {
|
|
|
|
using namespace google::protobuf;
|
|
|
|
constexpr int32_t sCurrentFileVersion = 1;
|
|
constexpr int32_t sHeaderSize = 4;
|
|
static_assert(sizeof(sCurrentFileVersion) == sHeaderSize, "Header size is wrong");
|
|
|
|
constexpr int sHistogramSize = ProfileData::HistogramSize();
|
|
|
|
static bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto,
|
|
const std::string& package, int64_t versionCode,
|
|
int64_t startTime, int64_t endTime, const ProfileData* data);
|
|
static void dumpAsTextToFd(protos::GraphicsStatsProto* proto, int outFd);
|
|
|
|
class FileDescriptor {
|
|
public:
|
|
explicit FileDescriptor(int fd) : mFd(fd) {}
|
|
~FileDescriptor() {
|
|
if (mFd != -1) {
|
|
close(mFd);
|
|
mFd = -1;
|
|
}
|
|
}
|
|
bool valid() { return mFd != -1; }
|
|
operator int() { return mFd; } // NOLINT(google-explicit-constructor)
|
|
|
|
private:
|
|
int mFd;
|
|
};
|
|
|
|
class FileOutputStreamLite : public io::ZeroCopyOutputStream {
|
|
public:
|
|
explicit FileOutputStreamLite(int fd) : mCopyAdapter(fd), mImpl(&mCopyAdapter) {}
|
|
virtual ~FileOutputStreamLite() {}
|
|
|
|
int GetErrno() { return mCopyAdapter.mErrno; }
|
|
|
|
virtual bool Next(void** data, int* size) override { return mImpl.Next(data, size); }
|
|
|
|
virtual void BackUp(int count) override { mImpl.BackUp(count); }
|
|
|
|
virtual int64 ByteCount() const override { return mImpl.ByteCount(); }
|
|
|
|
bool Flush() { return mImpl.Flush(); }
|
|
|
|
private:
|
|
struct FDAdapter : public io::CopyingOutputStream {
|
|
int mFd;
|
|
int mErrno = 0;
|
|
|
|
explicit FDAdapter(int fd) : mFd(fd) {}
|
|
virtual ~FDAdapter() {}
|
|
|
|
virtual bool Write(const void* buffer, int size) override {
|
|
int ret;
|
|
while (size) {
|
|
ret = TEMP_FAILURE_RETRY(write(mFd, buffer, size));
|
|
if (ret <= 0) {
|
|
mErrno = errno;
|
|
return false;
|
|
}
|
|
size -= ret;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
FileOutputStreamLite::FDAdapter mCopyAdapter;
|
|
io::CopyingOutputStreamAdaptor mImpl;
|
|
};
|
|
|
|
bool GraphicsStatsService::parseFromFile(const std::string& path,
|
|
protos::GraphicsStatsProto* output) {
|
|
FileDescriptor fd{open(path.c_str(), O_RDONLY)};
|
|
if (!fd.valid()) {
|
|
int err = errno;
|
|
// The file not existing is normal for addToDump(), so only log if
|
|
// we get an unexpected error
|
|
if (err != ENOENT) {
|
|
ALOGW("Failed to open '%s', errno=%d (%s)", path.c_str(), err, strerror(err));
|
|
}
|
|
return false;
|
|
}
|
|
struct stat sb;
|
|
if (fstat(fd, &sb) || sb.st_size < sHeaderSize) {
|
|
int err = errno;
|
|
// The file not existing is normal for addToDump(), so only log if
|
|
// we get an unexpected error
|
|
if (err != ENOENT) {
|
|
ALOGW("Failed to fstat '%s', errno=%d (%s) (st_size %d)", path.c_str(), err,
|
|
strerror(err), (int)sb.st_size);
|
|
}
|
|
return false;
|
|
}
|
|
void* addr = mmap(nullptr, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
|
if (addr == MAP_FAILED) {
|
|
int err = errno;
|
|
// The file not existing is normal for addToDump(), so only log if
|
|
// we get an unexpected error
|
|
if (err != ENOENT) {
|
|
ALOGW("Failed to mmap '%s', errno=%d (%s)", path.c_str(), err, strerror(err));
|
|
}
|
|
return false;
|
|
}
|
|
uint32_t file_version = *reinterpret_cast<uint32_t*>(addr);
|
|
if (file_version != sCurrentFileVersion) {
|
|
ALOGW("file_version mismatch! expected %d got %d", sCurrentFileVersion, file_version);
|
|
munmap(addr, sb.st_size);
|
|
return false;
|
|
}
|
|
|
|
void* data = reinterpret_cast<uint8_t*>(addr) + sHeaderSize;
|
|
int dataSize = sb.st_size - sHeaderSize;
|
|
io::ArrayInputStream input{data, dataSize};
|
|
bool success = output->ParseFromZeroCopyStream(&input);
|
|
if (!success) {
|
|
ALOGW("Parse failed on '%s' error='%s'", path.c_str(),
|
|
output->InitializationErrorString().c_str());
|
|
}
|
|
munmap(addr, sb.st_size);
|
|
return success;
|
|
}
|
|
|
|
bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto, const std::string& package,
|
|
int64_t versionCode, int64_t startTime, int64_t endTime,
|
|
const ProfileData* data) {
|
|
if (proto->stats_start() == 0 || proto->stats_start() > startTime) {
|
|
proto->set_stats_start(startTime);
|
|
}
|
|
if (proto->stats_end() == 0 || proto->stats_end() < endTime) {
|
|
proto->set_stats_end(endTime);
|
|
}
|
|
proto->set_package_name(package);
|
|
proto->set_version_code(versionCode);
|
|
auto summary = proto->mutable_summary();
|
|
summary->set_total_frames(summary->total_frames() + data->totalFrameCount());
|
|
summary->set_janky_frames(summary->janky_frames() + data->jankFrameCount());
|
|
summary->set_missed_vsync_count(summary->missed_vsync_count() +
|
|
data->jankTypeCount(kMissedVsync));
|
|
summary->set_high_input_latency_count(summary->high_input_latency_count() +
|
|
data->jankTypeCount(kHighInputLatency));
|
|
summary->set_slow_ui_thread_count(summary->slow_ui_thread_count() +
|
|
data->jankTypeCount(kSlowUI));
|
|
summary->set_slow_bitmap_upload_count(summary->slow_bitmap_upload_count() +
|
|
data->jankTypeCount(kSlowSync));
|
|
summary->set_slow_draw_count(summary->slow_draw_count() + data->jankTypeCount(kSlowRT));
|
|
summary->set_missed_deadline_count(summary->missed_deadline_count()
|
|
+ data->jankTypeCount(kMissedDeadline));
|
|
|
|
bool creatingHistogram = false;
|
|
if (proto->histogram_size() == 0) {
|
|
proto->mutable_histogram()->Reserve(sHistogramSize);
|
|
creatingHistogram = true;
|
|
} else if (proto->histogram_size() != sHistogramSize) {
|
|
ALOGE("Histogram size mismatch, proto is %d expected %d", proto->histogram_size(),
|
|
sHistogramSize);
|
|
return false;
|
|
}
|
|
int index = 0;
|
|
bool hitMergeError = false;
|
|
data->histogramForEach([&](ProfileData::HistogramEntry entry) {
|
|
if (hitMergeError) return;
|
|
|
|
protos::GraphicsStatsHistogramBucketProto* bucket;
|
|
if (creatingHistogram) {
|
|
bucket = proto->add_histogram();
|
|
bucket->set_render_millis(entry.renderTimeMs);
|
|
} else {
|
|
bucket = proto->mutable_histogram(index);
|
|
if (bucket->render_millis() != static_cast<int32_t>(entry.renderTimeMs)) {
|
|
ALOGW("Frame time mistmatch %d vs. %u", bucket->render_millis(),
|
|
entry.renderTimeMs);
|
|
hitMergeError = true;
|
|
return;
|
|
}
|
|
}
|
|
bucket->set_frame_count(bucket->frame_count() + entry.frameCount);
|
|
index++;
|
|
});
|
|
return !hitMergeError;
|
|
}
|
|
|
|
static int32_t findPercentile(protos::GraphicsStatsProto* proto, int percentile) {
|
|
int32_t pos = percentile * proto->summary().total_frames() / 100;
|
|
int32_t remaining = proto->summary().total_frames() - pos;
|
|
for (auto it = proto->histogram().rbegin(); it != proto->histogram().rend(); ++it) {
|
|
remaining -= it->frame_count();
|
|
if (remaining <= 0) {
|
|
return it->render_millis();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void dumpAsTextToFd(protos::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: %" PRId64, proto->version_code());
|
|
dprintf(fd, "\nStats since: %" PRId64 "ns", proto->stats_start());
|
|
dprintf(fd, "\nStats end: %" PRId64 "ns", 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, "\nNumber Frame deadline missed: %d", summary.missed_deadline_count());
|
|
dprintf(fd, "\nHISTOGRAM:");
|
|
for (const auto& it : proto->histogram()) {
|
|
dprintf(fd, " %dms=%d", it.render_millis(), it.frame_count());
|
|
}
|
|
dprintf(fd, "\n");
|
|
}
|
|
|
|
void GraphicsStatsService::saveBuffer(const std::string& path, const std::string& package,
|
|
int64_t versionCode, int64_t startTime, int64_t endTime,
|
|
const ProfileData* data) {
|
|
protos::GraphicsStatsProto statsProto;
|
|
if (!parseFromFile(path, &statsProto)) {
|
|
statsProto.Clear();
|
|
}
|
|
if (!mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data)) {
|
|
return;
|
|
}
|
|
// Although we might not have read any data from the file, merging the existing data
|
|
// should always fully-initialize the proto
|
|
if (!statsProto.IsInitialized()) {
|
|
ALOGE("proto initialization error %s", statsProto.InitializationErrorString().c_str());
|
|
return;
|
|
}
|
|
if (statsProto.package_name().empty() || !statsProto.has_summary()) {
|
|
ALOGE("missing package_name() '%s' summary %d", statsProto.package_name().c_str(),
|
|
statsProto.has_summary());
|
|
return;
|
|
}
|
|
int outFd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0660);
|
|
if (outFd <= 0) {
|
|
int err = errno;
|
|
ALOGW("Failed to open '%s', error=%d (%s)", path.c_str(), err, strerror(err));
|
|
return;
|
|
}
|
|
int wrote = write(outFd, &sCurrentFileVersion, sHeaderSize);
|
|
if (wrote != sHeaderSize) {
|
|
int err = errno;
|
|
ALOGW("Failed to write header to '%s', returned=%d errno=%d (%s)", path.c_str(), wrote, err,
|
|
strerror(err));
|
|
close(outFd);
|
|
return;
|
|
}
|
|
{
|
|
FileOutputStreamLite output(outFd);
|
|
bool success = statsProto.SerializeToZeroCopyStream(&output) && output.Flush();
|
|
if (output.GetErrno() != 0) {
|
|
ALOGW("Error writing to fd=%d, path='%s' err=%d (%s)", outFd, path.c_str(),
|
|
output.GetErrno(), strerror(output.GetErrno()));
|
|
success = false;
|
|
} else if (!success) {
|
|
ALOGW("Serialize failed on '%s' unknown error", path.c_str());
|
|
}
|
|
}
|
|
close(outFd);
|
|
}
|
|
|
|
class GraphicsStatsService::Dump {
|
|
public:
|
|
Dump(int outFd, DumpType type) : mFd(outFd), mType(type) {}
|
|
int fd() { return mFd; }
|
|
DumpType type() { return mType; }
|
|
protos::GraphicsStatsServiceDumpProto& proto() { return mProto; }
|
|
|
|
private:
|
|
int mFd;
|
|
DumpType mType;
|
|
protos::GraphicsStatsServiceDumpProto mProto;
|
|
};
|
|
|
|
GraphicsStatsService::Dump* GraphicsStatsService::createDump(int outFd, DumpType type) {
|
|
return new Dump(outFd, type);
|
|
}
|
|
|
|
void GraphicsStatsService::addToDump(Dump* dump, const std::string& path,
|
|
const std::string& package, int64_t versionCode,
|
|
int64_t startTime, int64_t endTime, const ProfileData* data) {
|
|
protos::GraphicsStatsProto statsProto;
|
|
if (!path.empty() && !parseFromFile(path, &statsProto)) {
|
|
statsProto.Clear();
|
|
}
|
|
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 {
|
|
dumpAsTextToFd(&statsProto, dump->fd());
|
|
}
|
|
}
|
|
|
|
void GraphicsStatsService::addToDump(Dump* dump, const std::string& path) {
|
|
protos::GraphicsStatsProto statsProto;
|
|
if (!parseFromFile(path, &statsProto)) {
|
|
return;
|
|
}
|
|
if (dump->type() == DumpType::Protobuf) {
|
|
dump->proto().add_stats()->CopyFrom(statsProto);
|
|
} else {
|
|
dumpAsTextToFd(&statsProto, dump->fd());
|
|
}
|
|
}
|
|
|
|
void GraphicsStatsService::finishDump(Dump* dump) {
|
|
if (dump->type() == DumpType::Protobuf) {
|
|
FileOutputStreamLite stream(dump->fd());
|
|
dump->proto().SerializeToZeroCopyStream(&stream);
|
|
}
|
|
delete dump;
|
|
}
|
|
|
|
} /* namespace uirenderer */
|
|
} /* namespace android */
|