Overhaul GraphicsStatsService

* LRU cache of recently-used is dead, replaced
  disk storage
* ASHMEM size is read from native by the system service,
  no longer requires keeping a sizeof() in sync with a
  constant in Java
* Supports dumping in proto format by passing --proto
* Rotates logs on a daily basis
* Keeps a history of the most recent 3 days

Bug: 33705836
Test: Manual. Verified log rotating works by setting it up to
rotate every minute instead of day. Confirmed /data/system/graphicsstats
only has the most recent 3 entries after several minutes

Change-Id: Ib84bafb26c58701cc86f123236de4fff01aaa4aa
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index ff40c8a..9515b82 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -47,6 +47,7 @@
     renderthread/RenderThread.cpp \
     renderthread/TimeLord.cpp \
     renderthread/Frame.cpp \
+    service/GraphicsStatsService.cpp \
     thread/TaskManager.cpp \
     utils/Blur.cpp \
     utils/GLUtils.cpp \
@@ -293,6 +294,7 @@
     tests/unit/GlopBuilderTests.cpp \
     tests/unit/GpuMemoryTrackerTests.cpp \
     tests/unit/GradientCacheTests.cpp \
+    tests/unit/GraphicsStatsServiceTests.cpp \
     tests/unit/LayerUpdateQueueTests.cpp \
     tests/unit/LeakCheckTests.cpp \
     tests/unit/LinearAllocatorTests.cpp \
diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp
index 2132c2b..7be71ee 100644
--- a/libs/hwui/JankTracker.cpp
+++ b/libs/hwui/JankTracker.cpp
@@ -110,7 +110,7 @@
 }
 
 // Only called when dumping stats, less performance sensitive
-static uint32_t frameTimeForFrameCountIndex(uint32_t index) {
+int32_t JankTracker::frameTimeForFrameCountIndex(uint32_t index) {
     index = index + kBucketMinThreshold;
     if (index > kBucket2msIntervals) {
         index += (index - kBucket2msIntervals);
@@ -123,6 +123,10 @@
     return index;
 }
 
+int32_t JankTracker::frameTimeForSlowFrameCountIndex(uint32_t index) {
+    return (index * kSlowFrameBucketIntervalMs) + kSlowFrameBucketStartMs;
+}
+
 JankTracker::JankTracker(const DisplayInfo& displayInfo) {
     // By default this will use malloc memory. It may be moved later to ashmem
     // if there is shared space for it and a request comes in to do that.
@@ -161,8 +165,25 @@
     mData = nullptr;
 }
 
+void JankTracker::rotateStorage() {
+    // If we are mapped we want to stop using the ashmem backend and switch to malloc
+    // We are expecting a switchStorageToAshmem call to follow this, but it's not guaranteed
+    // If we aren't sitting on top of ashmem then just do a reset() as it's functionally
+    // equivalent do a free, malloc, reset.
+    if (mIsMapped) {
+        freeData();
+        mData = new ProfileData;
+    }
+    reset();
+}
+
 void JankTracker::switchStorageToAshmem(int ashmemfd) {
     int regionSize = ashmem_get_size_region(ashmemfd);
+    if (regionSize < 0) {
+        int err = errno;
+        ALOGW("Failed to get ashmem region size from fd %d, err %d %s", ashmemfd, err, strerror(err));
+        return;
+    }
     if (regionSize < static_cast<int>(sizeof(ProfileData))) {
         ALOGW("Ashmem region is too small! Received %d, required %u",
                 regionSize, static_cast<unsigned int>(sizeof(ProfileData)));
@@ -279,15 +300,19 @@
     }
 }
 
-void JankTracker::dumpBuffer(const void* buffer, size_t bufsize, int fd) {
-    if (bufsize < sizeof(ProfileData)) {
-        return;
+void JankTracker::dumpData(int fd, const ProfileDataDescription* description, const ProfileData* data) {
+    if (description) {
+        switch (description->type) {
+            case JankTrackerType::Generic:
+                break;
+            case JankTrackerType::Package:
+                dprintf(fd, "\nPackage: %s", description->name.c_str());
+                break;
+            case JankTrackerType::Window:
+                dprintf(fd, "\nWindow: %s", description->name.c_str());
+                break;
+        }
     }
-    const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer);
-    dumpData(data, fd);
-}
-
-void JankTracker::dumpData(const ProfileData* data, int fd) {
     if (sFrameStart != FrameInfoIndex::IntendedVsync) {
         dprintf(fd, "\nNote: Data has been filtered!");
     }
@@ -308,7 +333,7 @@
                 data->frameCounts[i]);
     }
     for (size_t i = 0; i < data->slowFrameCounts.size(); i++) {
-        dprintf(fd, " %zums=%u", (i * kSlowFrameBucketIntervalMs) + kSlowFrameBucketStartMs,
+        dprintf(fd, " %ums=%u", frameTimeForSlowFrameCountIndex(i),
                 data->slowFrameCounts[i]);
     }
     dprintf(fd, "\n");
diff --git a/libs/hwui/JankTracker.h b/libs/hwui/JankTracker.h
index 8b482d5..6ff5d89 100644
--- a/libs/hwui/JankTracker.h
+++ b/libs/hwui/JankTracker.h
@@ -54,29 +54,50 @@
     nsecs_t statStartTime;
 };
 
+enum class JankTrackerType {
+    // The default, means there's no description set
+    Generic,
+    // The profile data represents a package
+    Package,
+    // The profile data is for a specific window
+    Window,
+};
+
+// Metadata about the ProfileData being collected
+struct ProfileDataDescription {
+    JankTrackerType type;
+    std::string name;
+};
+
 // TODO: Replace DrawProfiler with this
 class JankTracker {
 public:
     explicit JankTracker(const DisplayInfo& displayInfo);
     ~JankTracker();
 
+    void setDescription(JankTrackerType type, const std::string&& name) {
+        mDescription.type = type;
+        mDescription.name = name;
+    }
+
     void addFrame(const FrameInfo& frame);
 
-    void dump(int fd) { dumpData(mData, fd); }
+    void dump(int fd) { dumpData(fd, &mDescription, mData); }
     void reset();
 
+    void rotateStorage();
     void switchStorageToAshmem(int ashmemfd);
 
     uint32_t findPercentile(int p) { return findPercentile(mData, p); }
-
-    ANDROID_API static void dumpBuffer(const void* buffer, size_t bufsize, int fd);
+    static int32_t frameTimeForFrameCountIndex(uint32_t index);
+    static int32_t frameTimeForSlowFrameCountIndex(uint32_t index);
 
 private:
     void freeData();
     void setFrameInterval(nsecs_t frameIntervalNanos);
 
     static uint32_t findPercentile(const ProfileData* data, int p);
-    static void dumpData(const ProfileData* data, int fd);
+    static void dumpData(int fd, const ProfileDataDescription* description, const ProfileData* data);
 
     std::array<int64_t, NUM_BUCKETS> mThresholds;
     int64_t mFrameInterval;
@@ -90,6 +111,7 @@
     nsecs_t mDequeueTimeForgiveness = 0;
     ProfileData* mData;
     bool mIsMapped = false;
+    ProfileDataDescription mDescription;
 };
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/hwui_static_deps.mk b/libs/hwui/hwui_static_deps.mk
index a75fd6a..8826cfc 100644
--- a/libs/hwui/hwui_static_deps.mk
+++ b/libs/hwui/hwui_static_deps.mk
@@ -22,11 +22,12 @@
     libskia \
     libui \
     libgui \
-    libprotobuf-cpp-lite \
+    libprotobuf-cpp-full \
     libharfbuzz_ng \
     libft2 \
     libminikin \
-    libandroidfw
+    libandroidfw \
+    libRScpp
 
-# enable RENDERSCRIPT
-LOCAL_SHARED_LIBRARIES += libRScpp
+LOCAL_STATIC_LIBRARIES += \
+    libplatformprotos
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index a53e5e0..02a9ffa 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -590,6 +590,7 @@
 }
 
 void CanvasContext::dumpFrames(int fd) {
+    mJankTracker.dump(fd);
     FILE* file = fdopen(fd, "a");
     fprintf(file, "\n\n---PROFILEDATA---\n");
     for (size_t i = 0; i < static_cast<size_t>(FrameInfoIndex::NumIndexes); i++) {
@@ -615,6 +616,10 @@
     mRenderThread.jankTracker().reset();
 }
 
+void CanvasContext::setName(const std::string&& name) {
+    mJankTracker.setDescription(JankTrackerType::Window, std::move(name));
+}
+
 void CanvasContext::serializeDisplayListTree() {
 #if ENABLE_RENDERNODE_SERIALIZATION
     using namespace google::protobuf::io;
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index aa01caa..738c091 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -155,8 +155,7 @@
     void dumpFrames(int fd);
     void resetFrameStats();
 
-    void setName(const std::string&& name) { mName = name; }
-    const std::string& name() { return mName; }
+    void setName(const std::string&& name);
 
     void serializeDisplayListTree();
 
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 11614fa..f4a4773 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -487,9 +487,22 @@
 
 void RenderProxy::setProcessStatsBuffer(int fd) {
     SETUP_TASK(setProcessStatsBuffer);
-    args->thread = &mRenderThread;
+    auto& rt = RenderThread::getInstance();
+    args->thread = &rt;
     args->fd = dup(fd);
-    post(task);
+    rt.queue(task);
+}
+
+CREATE_BRIDGE1(rotateProcessStatsBuffer, RenderThread* thread) {
+    args->thread->jankTracker().rotateStorage();
+    return nullptr;
+}
+
+void RenderProxy::rotateProcessStatsBuffer() {
+    SETUP_TASK(rotateProcessStatsBuffer);
+    auto& rt = RenderThread::getInstance();
+    args->thread = &rt;
+    rt.queue(task);
 }
 
 int RenderProxy::getRenderThreadTid() {
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 1629090..a60ed55 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -113,7 +113,8 @@
     uint32_t frameTimePercentile(int p);
     ANDROID_API static void dumpGraphicsMemory(int fd);
 
-    ANDROID_API void setProcessStatsBuffer(int fd);
+    ANDROID_API static void rotateProcessStatsBuffer();
+    ANDROID_API static void setProcessStatsBuffer(int fd);
     ANDROID_API int getRenderThreadTid();
 
     ANDROID_API void serializeDisplayListTree();
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index d121bcf..9bc5985 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -74,6 +74,7 @@
 };
 
 class ANDROID_API RenderThread : public Thread {
+    PREVENT_COPY_AND_ASSIGN(RenderThread);
 public:
     // RenderThread takes complete ownership of tasks that are queued
     // and will delete them after they are run
diff --git a/libs/hwui/service/GraphicsStatsService.cpp b/libs/hwui/service/GraphicsStatsService.cpp
new file mode 100644
index 0000000..ab6420e
--- /dev/null
+++ b/libs/hwui/service/GraphicsStatsService.cpp
@@ -0,0 +1,287 @@
+/*
+ * 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 <frameworks/base/core/proto/android/service/graphicsstats.pb.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <log/log.h>
+
+#include <inttypes.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.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 =
+        std::tuple_size<decltype(ProfileData::frameCounts)>::value +
+        std::tuple_size<decltype(ProfileData::slowFrameCounts)>::value;
+
+static void mergeProfileDataIntoProto(service::GraphicsStatsProto* proto,
+        const std::string& package, int versionCode, int64_t startTime, int64_t endTime,
+        const ProfileData* data);
+static void dumpAsTextToFd(service::GraphicsStatsProto* proto, int outFd);
+
+bool GraphicsStatsService::parseFromFile(const std::string& path, service::GraphicsStatsProto* output) {
+
+    int fd = open(path.c_str(), O_RDONLY);
+    if (fd == -1) {
+        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;
+    }
+    uint32_t file_version;
+    ssize_t bytesRead = read(fd, &file_version, sHeaderSize);
+    if (bytesRead != sHeaderSize || file_version != sCurrentFileVersion) {
+        ALOGW("Failed to read '%s', bytesRead=%zd file_version=%d", path.c_str(), bytesRead,
+                file_version);
+        close(fd);
+        return false;
+    }
+
+    io::FileInputStream input(fd);
+    bool success = output->ParseFromZeroCopyStream(&input);
+    if (input.GetErrno() != 0) {
+        ALOGW("Error reading from fd=%d, path='%s' err=%d (%s)",
+                fd, path.c_str(), input.GetErrno(), strerror(input.GetErrno()));
+        success = false;
+    } else if (!success) {
+        ALOGW("Parse failed on '%s' error='%s'",
+                path.c_str(), output->InitializationErrorString().c_str());
+    }
+    close(fd);
+    return success;
+}
+
+void mergeProfileDataIntoProto(service::GraphicsStatsProto* proto, const std::string& package,
+        int 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->jankTypeCounts[kMissedVsync]);
+    summary->set_high_input_latency_count(
+            summary->high_input_latency_count() + data->jankTypeCounts[kHighInputLatency]);
+    summary->set_slow_ui_thread_count(
+            summary->slow_ui_thread_count() + data->jankTypeCounts[kSlowUI]);
+    summary->set_slow_bitmap_upload_count(
+            summary->slow_bitmap_upload_count() + data->jankTypeCounts[kSlowSync]);
+    summary->set_slow_draw_count(
+            summary->slow_draw_count() + data->jankTypeCounts[kSlowRT]);
+
+    bool creatingHistogram = false;
+    if (proto->histogram_size() == 0) {
+        proto->mutable_histogram()->Reserve(sHistogramSize);
+        creatingHistogram = true;
+    } else if (proto->histogram_size() != sHistogramSize) {
+        LOG_ALWAYS_FATAL("Histogram size mismatch, proto is %d expected %d",
+                proto->histogram_size(), sHistogramSize);
+    }
+    for (size_t i = 0; i < data->frameCounts.size(); i++) {
+        service::GraphicsStatsHistogramBucketProto* bucket;
+        int32_t renderTime = JankTracker::frameTimeForFrameCountIndex(i);
+        if (creatingHistogram) {
+            bucket = proto->add_histogram();
+            bucket->set_render_millis(renderTime);
+        } else {
+            bucket = proto->mutable_histogram(i);
+            LOG_ALWAYS_FATAL_IF(bucket->render_millis() != renderTime,
+                    "Frame time mistmatch %d vs. %d", bucket->render_millis(), renderTime);
+        }
+        bucket->set_frame_count(bucket->frame_count() + data->frameCounts[i]);
+    }
+    for (size_t i = 0; i < data->slowFrameCounts.size(); i++) {
+        service::GraphicsStatsHistogramBucketProto* bucket;
+        int32_t renderTime = JankTracker::frameTimeForSlowFrameCountIndex(i);
+        if (creatingHistogram) {
+            bucket = proto->add_histogram();
+            bucket->set_render_millis(renderTime);
+        } else {
+            constexpr int offset = std::tuple_size<decltype(ProfileData::frameCounts)>::value;
+            bucket = proto->mutable_histogram(offset + i);
+            LOG_ALWAYS_FATAL_IF(bucket->render_millis() != renderTime,
+                    "Frame time mistmatch %d vs. %d", bucket->render_millis(), renderTime);
+        }
+        bucket->set_frame_count(bucket->frame_count() + data->slowFrameCounts[i]);
+    }
+}
+
+static int32_t findPercentile(service::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(service::GraphicsStatsProto* proto, int fd) {
+    // This isn't a full validation, just enough that we can deref at will
+    LOG_ALWAYS_FATAL_IF(proto->package_name().empty()
+            || !proto->has_summary(), "package_name() '%s' summary %d",
+            proto->package_name().c_str(), proto->has_summary());
+    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");
+}
+
+void GraphicsStatsService::saveBuffer(const std::string& path, const std::string& package,
+        int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
+    service::GraphicsStatsProto statsProto;
+    if (!parseFromFile(path, &statsProto)) {
+        statsProto.Clear();
+    }
+    mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data);
+    // Although we might not have read any data from the file, merging the existing data
+    // should always fully-initialize the proto
+    LOG_ALWAYS_FATAL_IF(!statsProto.IsInitialized(), "%s",
+            statsProto.InitializationErrorString().c_str());
+    LOG_ALWAYS_FATAL_IF(statsProto.package_name().empty()
+            || !statsProto.has_summary(), "package_name() '%s' summary %d",
+            statsProto.package_name().c_str(), statsProto.has_summary());
+    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;
+    }
+    {
+        io::FileOutputStream 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; }
+    service::GraphicsStatsServiceDumpProto& proto() { return mProto; }
+private:
+    int mFd;
+    DumpType mType;
+    service::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,
+        int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
+    service::GraphicsStatsProto statsProto;
+    if (!path.empty() && !parseFromFile(path, &statsProto)) {
+        statsProto.Clear();
+    }
+    if (data) {
+        mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data);
+    }
+    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) {
+    service::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) {
+        io::FileOutputStream stream(dump->fd());
+        dump->proto().SerializeToZeroCopyStream(&stream);
+    }
+    delete dump;
+}
+
+} /* namespace uirenderer */
+} /* namespace android */
\ No newline at end of file
diff --git a/libs/hwui/service/GraphicsStatsService.h b/libs/hwui/service/GraphicsStatsService.h
new file mode 100644
index 0000000..d0fd60e
--- /dev/null
+++ b/libs/hwui/service/GraphicsStatsService.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string>
+
+#include "JankTracker.h"
+#include "utils/Macros.h"
+
+namespace android {
+namespace service {
+class GraphicsStatsProto;
+}
+
+namespace uirenderer {
+
+/*
+ * The exported entry points used by GraphicsStatsService.java in f/b/services/core
+ *
+ * NOTE: Avoid exporting a requirement on the protobuf itself. Keep the usage
+ * of the generated protobuf classes internal to libhwui.so to minimize library
+ * bloat.
+ */
+class GraphicsStatsService {
+public:
+    class Dump;
+    enum class DumpType {
+        Text,
+        Protobuf,
+    };
+
+    ANDROID_API static void saveBuffer(const std::string& path, const std::string& package,
+            int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data);
+
+    ANDROID_API static Dump* createDump(int outFd, DumpType type);
+    ANDROID_API static void addToDump(Dump* dump, const std::string& path, const std::string& package,
+            int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data);
+    ANDROID_API static void addToDump(Dump* dump, const std::string& path);
+    ANDROID_API static void finishDump(Dump* dump);
+
+    // Visible for testing
+    static bool parseFromFile(const std::string& path, service::GraphicsStatsProto* output);
+};
+
+} /* namespace uirenderer */
+} /* namespace android */
\ No newline at end of file
diff --git a/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
new file mode 100644
index 0000000..cfe1134
--- /dev/null
+++ b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 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 <gtest/gtest.h>
+
+#include "service/GraphicsStatsService.h"
+
+#include <frameworks/base/core/proto/android/service/graphicsstats.pb.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+using namespace android;
+using namespace android::uirenderer;
+
+std::string findRootPath() {
+    char path[1024];
+    ssize_t r = readlink("/proc/self/exe", path, 1024);
+    // < 1023 because we need room for the null terminator
+    if (r <= 0 || r > 1023) {
+        int err = errno;
+        fprintf(stderr, "Failed to read from /proc/self/exe; r=%zd, err=%d (%s)\n",
+                r, err, strerror(err));
+        exit(EXIT_FAILURE);
+    }
+    while (--r > 0) {
+        if (path[r] == '/') {
+            path[r] = '\0';
+            return std::string(path);
+        }
+    }
+    return std::string();
+}
+
+// No code left untested
+TEST(GraphicsStats, findRootPath) {
+    std::string expected = "/data/nativetest/hwui_unit_tests";
+    EXPECT_EQ(expected, findRootPath());
+}
+
+TEST(GraphicsStats, saveLoad) {
+    std::string path = findRootPath() + "/test_saveLoad";
+    std::string packageName = "com.test.saveLoad";
+    ProfileData mockData;
+    mockData.jankFrameCount = 20;
+    mockData.totalFrameCount = 100;
+    mockData.statStartTime = 10000;
+    // Fill with patterned data we can recognize but which won't map to a
+    // memset or basic for iteration count
+    for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+        mockData.frameCounts[i] = ((i % 10) + 1) * 2;
+    }
+    for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+        mockData.slowFrameCounts[i] = (i % 5) + 1;
+    }
+    GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData);
+    service::GraphicsStatsProto loadedProto;
+    EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto));
+    // Clean up the file
+    unlink(path.c_str());
+
+    EXPECT_EQ(packageName, loadedProto.package_name());
+    EXPECT_EQ(5, loadedProto.version_code());
+    EXPECT_EQ(3000, loadedProto.stats_start());
+    EXPECT_EQ(7000, loadedProto.stats_end());
+    // ASSERT here so we don't continue with a nullptr deref crash if this is false
+    ASSERT_TRUE(loadedProto.has_summary());
+    EXPECT_EQ(20, loadedProto.summary().janky_frames());
+    EXPECT_EQ(100, loadedProto.summary().total_frames());
+    EXPECT_EQ(mockData.frameCounts.size() + mockData.slowFrameCounts.size(),
+            (size_t) loadedProto.histogram_size());
+    for (size_t i = 0; i < (size_t) loadedProto.histogram_size(); i++) {
+        int expectedCount, expectedBucket;
+        if (i < mockData.frameCounts.size()) {
+            expectedCount = ((i % 10) + 1) * 2;
+            expectedBucket = JankTracker::frameTimeForFrameCountIndex(i);
+        } else {
+            int temp = i - mockData.frameCounts.size();
+            expectedCount = (temp % 5) + 1;
+            expectedBucket = JankTracker::frameTimeForSlowFrameCountIndex(temp);
+        }
+        EXPECT_EQ(expectedCount, loadedProto.histogram().Get(i).frame_count());
+        EXPECT_EQ(expectedBucket, loadedProto.histogram().Get(i).render_millis());
+    }
+}
+
+TEST(GraphicsStats, merge) {
+    std::string path = findRootPath() + "/test_merge";
+    std::string packageName = "com.test.merge";
+    ProfileData mockData;
+    mockData.jankFrameCount = 20;
+    mockData.totalFrameCount = 100;
+    mockData.statStartTime = 10000;
+    // Fill with patterned data we can recognize but which won't map to a
+    // memset or basic for iteration count
+    for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+        mockData.frameCounts[i] = ((i % 10) + 1) * 2;
+    }
+    for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+        mockData.slowFrameCounts[i] = (i % 5) + 1;
+    }
+    GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData);
+    mockData.jankFrameCount = 50;
+    mockData.totalFrameCount = 500;
+    for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+        mockData.frameCounts[i] = (i % 5) + 1;
+    }
+    for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+        mockData.slowFrameCounts[i] = ((i % 10) + 1) * 2;
+    }
+    GraphicsStatsService::saveBuffer(path, packageName, 5, 7050, 10000, &mockData);
+
+    service::GraphicsStatsProto loadedProto;
+    EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto));
+    // Clean up the file
+    unlink(path.c_str());
+
+    EXPECT_EQ(packageName, loadedProto.package_name());
+    EXPECT_EQ(5, loadedProto.version_code());
+    EXPECT_EQ(3000, loadedProto.stats_start());
+    EXPECT_EQ(10000, loadedProto.stats_end());
+    // ASSERT here so we don't continue with a nullptr deref crash if this is false
+    ASSERT_TRUE(loadedProto.has_summary());
+    EXPECT_EQ(20 + 50, loadedProto.summary().janky_frames());
+    EXPECT_EQ(100 + 500, loadedProto.summary().total_frames());
+    EXPECT_EQ(mockData.frameCounts.size() + mockData.slowFrameCounts.size(),
+            (size_t) loadedProto.histogram_size());
+    for (size_t i = 0; i < (size_t) loadedProto.histogram_size(); i++) {
+        int expectedCount, expectedBucket;
+        if (i < mockData.frameCounts.size()) {
+            expectedCount = ((i % 10) + 1) * 2;
+            expectedCount += (i % 5) + 1;
+            expectedBucket = JankTracker::frameTimeForFrameCountIndex(i);
+        } else {
+            int temp = i - mockData.frameCounts.size();
+            expectedCount = (temp % 5) + 1;
+            expectedCount += ((temp % 10) + 1) * 2;
+            expectedBucket = JankTracker::frameTimeForSlowFrameCountIndex(temp);
+        }
+        EXPECT_EQ(expectedCount, loadedProto.histogram().Get(i).frame_count());
+        EXPECT_EQ(expectedBucket, loadedProto.histogram().Get(i).render_millis());
+    }
+}
\ No newline at end of file