/* * 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 #include #include #include #include #include #include #include #include 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(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(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(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() ? "" : 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 */