Merge "Overhaul GraphicsStatsService"
diff --git a/Android.mk b/Android.mk
index 0741c818..b98d3bc 100644
--- a/Android.mk
+++ b/Android.mk
@@ -323,6 +323,7 @@
 	core/java/android/view/IAppTransitionAnimationSpecsFuture.aidl \
 	core/java/android/view/IDockedStackListener.aidl \
 	core/java/android/view/IGraphicsStats.aidl \
+	core/java/android/view/IGraphicsStatsCallback.aidl \
 	core/java/android/view/IInputFilter.aidl \
 	core/java/android/view/IInputFilterHost.aidl \
 	core/java/android/view/IOnKeyguardExitResult.aidl \
@@ -1423,6 +1424,24 @@
 
 include $(BUILD_JAVA_LIBRARY)
 
+# ====  c++ proto device library  ==============================
+include $(CLEAR_VARS)
+LOCAL_MODULE := libplatformprotos
+# b/34740546, work around clang-tidy segmentation fault.
+LOCAL_TIDY_CHECKS := -modernize*
+LOCAL_PROTOC_OPTIMIZE_TYPE := lite
+LOCAL_PROTOC_FLAGS := \
+    --include_source_info \
+    -Iexternal/protobuf/src
+LOCAL_SRC_FILES := \
+    $(call all-proto-files-under, core/proto) \
+    $(call all-proto-files-under, libs/incident/proto)
+LOCAL_C_INCLUDES := \
+    $(call generated-sources-dir-for,STATIC_LIBRARIES,libplatformprotos,)/proto
+LOCAL_EXPORT_C_INCLUDES := \
+    $(call generated-sources-dir-for,STATIC_LIBRARIES,libplatformprotos,)/proto
+include $(BUILD_STATIC_LIBRARY)
+
 # ====  c++ proto host library  ==============================
 include $(CLEAR_VARS)
 LOCAL_MODULE := libplatformprotos
diff --git a/core/java/android/view/IGraphicsStats.aidl b/core/java/android/view/IGraphicsStats.aidl
index c235eb2..e6b750b 100644
--- a/core/java/android/view/IGraphicsStats.aidl
+++ b/core/java/android/view/IGraphicsStats.aidl
@@ -17,10 +17,11 @@
 package android.view;
 
 import android.os.ParcelFileDescriptor;
+import android.view.IGraphicsStatsCallback;
 
 /**
  * @hide
  */
 interface IGraphicsStats {
-    ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token);
+    ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback callback);
 }
diff --git a/core/java/android/view/IGraphicsStatsCallback.aidl b/core/java/android/view/IGraphicsStatsCallback.aidl
new file mode 100644
index 0000000..f70e141
--- /dev/null
+++ b/core/java/android/view/IGraphicsStatsCallback.aidl
@@ -0,0 +1,24 @@
+/**
+ * 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.
+ */
+
+package android.view;
+
+/**
+ * @hide
+ */
+oneway interface IGraphicsStatsCallback {
+    void onRotateGraphicsStatsBuffer();
+}
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 217c152..c66bf874 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -25,9 +25,9 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.AnimatedVectorDrawable;
-import android.os.Binder;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Trace;
 import android.util.Log;
@@ -260,10 +260,10 @@
      *
      * @return A threaded renderer backed by OpenGL.
      */
-    public static ThreadedRenderer create(Context context, boolean translucent) {
+    public static ThreadedRenderer create(Context context, boolean translucent, String name) {
         ThreadedRenderer renderer = null;
         if (isAvailable()) {
-            renderer = new ThreadedRenderer(context, translucent);
+            renderer = new ThreadedRenderer(context, translucent, name);
         }
         return renderer;
     }
@@ -287,10 +287,6 @@
         nOverrideProperty(name, value);
     }
 
-    public static void dumpProfileData(byte[] data, FileDescriptor fd) {
-        nDumpProfileData(data, fd);
-    }
-
     // Keep in sync with DrawFrameTask.h SYNC_* flags
     // Nothing interesting to report
     private static final int SYNC_OK = 0;
@@ -346,7 +342,7 @@
     private boolean mEnabled;
     private boolean mRequested = true;
 
-    ThreadedRenderer(Context context, boolean translucent) {
+    ThreadedRenderer(Context context, boolean translucent, String name) {
         final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
         mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
         mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
@@ -360,6 +356,7 @@
         mRootNode = RenderNode.adopt(rootNodePtr);
         mRootNode.setClipToBounds(false);
         mNativeProxy = nCreateProxy(translucent, rootNodePtr);
+        nSetName(mNativeProxy, name);
 
         ProcessInitializer.sInstance.init(context, mNativeProxy);
 
@@ -827,15 +824,6 @@
     }
 
     /**
-     * Optional, sets the name of the renderer. Useful for debugging purposes.
-     *
-     * @param name The name of this renderer, can be null
-     */
-    void setName(String name) {
-        nSetName(mNativeProxy, name);
-    }
-
-    /**
      * Blocks until all previously queued work has completed.
      */
     void fence() {
@@ -896,20 +884,29 @@
 
     private static class ProcessInitializer {
         static ProcessInitializer sInstance = new ProcessInitializer();
-        private static IBinder sProcToken;
 
         private boolean mInitialized = false;
 
+        private Context mAppContext;
+        private IGraphicsStats mGraphicsStatsService;
+        private IGraphicsStatsCallback mGraphicsStatsCallback = new IGraphicsStatsCallback.Stub() {
+            @Override
+            public void onRotateGraphicsStatsBuffer() throws RemoteException {
+                rotateBuffer();
+            }
+        };
+
         private ProcessInitializer() {}
 
         synchronized void init(Context context, long renderProxy) {
             if (mInitialized) return;
             mInitialized = true;
+            mAppContext = context.getApplicationContext();
             initSched(context, renderProxy);
-            initGraphicsStats(context, renderProxy);
+            initGraphicsStats();
         }
 
-        private static void initSched(Context context, long renderProxy) {
+        private void initSched(Context context, long renderProxy) {
             try {
                 int tid = nGetRenderThreadTid(renderProxy);
                 ActivityManager.getService().setRenderThread(tid);
@@ -918,17 +915,28 @@
             }
         }
 
-        private static void initGraphicsStats(Context context, long renderProxy) {
+        private void initGraphicsStats() {
             try {
                 IBinder binder = ServiceManager.getService("graphicsstats");
                 if (binder == null) return;
-                IGraphicsStats graphicsStatsService = IGraphicsStats.Stub
-                        .asInterface(binder);
-                sProcToken = new Binder();
-                final String pkg = context.getApplicationInfo().packageName;
-                ParcelFileDescriptor pfd = graphicsStatsService.
-                        requestBufferForProcess(pkg, sProcToken);
-                nSetProcessStatsBuffer(renderProxy, pfd.getFd());
+                mGraphicsStatsService = IGraphicsStats.Stub.asInterface(binder);
+                requestBuffer();
+            } catch (Throwable t) {
+                Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
+            }
+        }
+
+        private void rotateBuffer() {
+            nRotateProcessStatsBuffer();
+            requestBuffer();
+        }
+
+        private void requestBuffer() {
+            try {
+                final String pkg = mAppContext.getApplicationInfo().packageName;
+                ParcelFileDescriptor pfd = mGraphicsStatsService
+                        .requestBufferForProcess(pkg, mGraphicsStatsCallback);
+                nSetProcessStatsBuffer(pfd.getFd());
                 pfd.close();
             } catch (Throwable t) {
                 Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
@@ -948,7 +956,8 @@
 
     static native void setupShadersDiskCache(String cacheFile);
 
-    private static native void nSetProcessStatsBuffer(long nativeProxy, int fd);
+    private static native void nRotateProcessStatsBuffer();
+    private static native void nSetProcessStatsBuffer(int fd);
     private static native int nGetRenderThreadTid(long nativeProxy);
 
     private static native long nCreateRootRenderNode();
@@ -993,7 +1002,6 @@
 
     private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd,
             @DumpFlags int dumpFlags);
-    private static native void nDumpProfileData(byte[] data, FileDescriptor fd);
 
     private static native void nAddRenderNode(long nativeProxy, long rootRenderNode,
              boolean placeFront);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 6cdd483..c81e938 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -887,9 +887,9 @@
                 final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0
                         || insets.top != 0 || insets.bottom != 0;
                 final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets;
-                mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent);
+                mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
+                        attrs.getTitle().toString());
                 if (mAttachInfo.mThreadedRenderer != null) {
-                    mAttachInfo.mThreadedRenderer.setName(attrs.getTitle().toString());
                     mAttachInfo.mHardwareAccelerated =
                             mAttachInfo.mHardwareAccelerationRequested = true;
                 }
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index d37f96a..37eae48a 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -43,7 +43,6 @@
 #include <FrameInfo.h>
 #include <FrameMetricsObserver.h>
 #include <IContextFactory.h>
-#include <JankTracker.h>
 #include <PropertyValuesAnimatorSet.h>
 #include <RenderNode.h>
 #include <renderthread/CanvasContext.h>
@@ -587,10 +586,13 @@
     return atoi(prop) > 0 ? JNI_TRUE : JNI_FALSE;
 }
 
+static void android_view_ThreadedRenderer_rotateProcessStatsBuffer(JNIEnv* env, jobject clazz) {
+    RenderProxy::rotateProcessStatsBuffer();
+}
+
 static void android_view_ThreadedRenderer_setProcessStatsBuffer(JNIEnv* env, jobject clazz,
-        jlong proxyPtr, jint fd) {
-    RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    proxy->setProcessStatsBuffer(fd);
+        jint fd) {
+    RenderProxy::setProcessStatsBuffer(fd);
 }
 
 static jint android_view_ThreadedRenderer_getRenderThreadTid(JNIEnv* env, jobject clazz,
@@ -817,15 +819,6 @@
     proxy->dumpProfileInfo(fd, dumpFlags);
 }
 
-static void android_view_ThreadedRenderer_dumpProfileData(JNIEnv* env, jobject clazz,
-        jbyteArray jdata, jobject javaFileDescriptor) {
-    int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor);
-    ScopedByteArrayRO buffer(env, jdata);
-    if (buffer.get()) {
-        JankTracker::dumpBuffer(buffer.get(), buffer.size(), fd);
-    }
-}
-
 static void android_view_ThreadedRenderer_addRenderNode(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jlong renderNodePtr, jboolean placeFront) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
@@ -910,7 +903,8 @@
 
 static const JNINativeMethod gMethods[] = {
     { "nSupportsOpenGL", "()Z", (void*) android_view_ThreadedRenderer_supportsOpenGL },
-    { "nSetProcessStatsBuffer", "(JI)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer },
+    { "nRotateProcessStatsBuffer", "()V", (void*) android_view_ThreadedRenderer_rotateProcessStatsBuffer },
+    { "nSetProcessStatsBuffer", "(I)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer },
     { "nGetRenderThreadTid", "(J)I", (void*) android_view_ThreadedRenderer_getRenderThreadTid },
     { "nCreateRootRenderNode", "()J", (void*) android_view_ThreadedRenderer_createRootRenderNode },
     { "nCreateProxy", "(ZJ)J", (void*) android_view_ThreadedRenderer_createProxy },
@@ -943,7 +937,6 @@
     { "nNotifyFramePending", "(J)V", (void*) android_view_ThreadedRenderer_notifyFramePending },
     { "nSerializeDisplayListTree", "(J)V", (void*) android_view_ThreadedRenderer_serializeDisplayListTree },
     { "nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo },
-    { "nDumpProfileData", "([BLjava/io/FileDescriptor;)V", (void*) android_view_ThreadedRenderer_dumpProfileData },
     { "setupShadersDiskCache", "(Ljava/lang/String;)V",
                 (void*) android_view_ThreadedRenderer_setupShadersDiskCache },
     { "nAddRenderNode", "(JJZ)V", (void*) android_view_ThreadedRenderer_addRenderNode},
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index ba1d664..a2f07d9 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -21,6 +21,7 @@
 
 import "frameworks/base/libs/incident/proto/android/privacy.proto";
 import "frameworks/base/core/proto/android/service/appwidget.proto";
+import "frameworks/base/core/proto/android/service/graphicsstats.proto";
 import "frameworks/base/core/proto/android/service/fingerprint.proto";
 import "frameworks/base/core/proto/android/service/netstats.proto";
 import "frameworks/base/core/proto/android/service/notification.proto";
@@ -57,4 +58,5 @@
     android.providers.settings.SettingsServiceDumpProto settings = 3002;
     android.service.appwidget.AppWidgetServiceDumpProto appwidget = 3003;
     android.service.notification.NotificationServiceDumpProto notification = 3004;
+    android.service.GraphicsStatsServiceDumpProto graphicsstats = 3005;
 }
diff --git a/core/proto/android/service/graphicsstats.proto b/core/proto/android/service/graphicsstats.proto
new file mode 100644
index 0000000..6dbfe48
--- /dev/null
+++ b/core/proto/android/service/graphicsstats.proto
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package android.service;
+
+option java_multiple_files = true;
+option java_outer_classname = "GraphicsStatsServiceProto";
+
+message GraphicsStatsServiceDumpProto {
+    repeated GraphicsStatsProto stats = 1;
+}
+
+message GraphicsStatsProto {
+
+    // The package name of the app
+    string package_name = 1;
+
+    // The version code of the app
+    int32 version_code = 2;
+
+    // The start & end timestamps in UTC as
+    // milliseconds since January 1, 1970
+    // Compatible with java.util.Date#setTime()
+    int64 stats_start = 3;
+    int64 stats_end = 4;
+
+    // The aggregated statistics for the package
+    GraphicsStatsJankSummaryProto summary = 5;
+
+    // The frame time histogram for the package
+    repeated GraphicsStatsHistogramBucketProto histogram = 6;
+}
+
+message GraphicsStatsJankSummaryProto {
+    // Distinct frame count.
+    int32 total_frames = 1;
+
+    // Number of frames with slow render time. Frames are considered janky if
+    // they took more than a vsync interval (typically 16.667ms) to be rendered.
+    int32 janky_frames = 2;
+
+    // Number of "missed vsync" events.
+    int32 missed_vsync_count = 3;
+
+    // Number of "high input latency" events.
+    int32 high_input_latency_count = 4;
+
+    // Number of "slow UI thread" events.
+    int32 slow_ui_thread_count = 5;
+
+    // Number of "slow bitmap upload" events.
+    int32 slow_bitmap_upload_count = 6;
+
+    // Number of "slow draw" events.
+    int32 slow_draw_count = 7;
+}
+
+message GraphicsStatsHistogramBucketProto {
+    // Lower bound of render time in milliseconds.
+    int32 render_millis = 1;
+    // Number of frames in the bucket.
+    int32 frame_count = 2;
+}
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
diff --git a/services/core/java/com/android/server/GraphicsStatsService.java b/services/core/java/com/android/server/GraphicsStatsService.java
index ecbe1ca..bdd80e38 100644
--- a/services/core/java/com/android/server/GraphicsStatsService.java
+++ b/services/core/java/com/android/server/GraphicsStatsService.java
@@ -16,67 +16,145 @@
 
 package com.android.server;
 
+import android.app.AlarmManager;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.os.Binder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.MemoryFile;
+import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
+import android.os.Trace;
 import android.util.Log;
 import android.view.IGraphicsStats;
-import android.view.ThreadedRenderer;
+import android.view.IGraphicsStatsCallback;
 
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.TimeZone;
 
 /**
  * This service's job is to collect aggregate rendering profile data. It
  * does this by allowing rendering processes to request an ashmem buffer
- * to place their stats into. This buffer will be pre-initialized with historical
- * data for that process if it exists (if the userId & packageName match a buffer
- * in the historical log)
+ * to place their stats into.
  *
- * This service does not itself attempt to understand the data in the buffer,
- * its primary job is merely to manage distributing these buffers. However,
- * it is assumed that this buffer is for ThreadedRenderer and delegates
- * directly to ThreadedRenderer for dumping buffers.
+ * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days
+ * are kept.
  *
- * MEMORY USAGE:
+ * The primary consumer of this is incident reports and automated metric checking. It is not
+ * intended for end-developer consumption, for that we have gfxinfo.
  *
- * This class consumes UP TO:
- * 1) [active rendering processes] * (ASHMEM_SIZE * 2)
- * 2) ASHMEM_SIZE (for scratch space used during dumping)
- * 3) ASHMEM_SIZE * HISTORY_SIZE
- *
- * This is currently under 20KiB total memory in the worst case of
- * 20 processes in history + 10 unique active processes.
+ * Buffer rotation process:
+ * 1) Alarm fires
+ * 2) onRotateGraphicsStatsBuffer() is sent to all active processes
+ * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and
+ *    request a new one.
+ * 4) When that request is received we now know that the ashmem region is no longer in use so
+ *    it gets queued up for saving to disk and a new ashmem region is created and returned
+ *    for the process to use.
  *
  *  @hide */
 public class GraphicsStatsService extends IGraphicsStats.Stub {
     public static final String GRAPHICS_STATS_SERVICE = "graphicsstats";
 
     private static final String TAG = "GraphicsStatsService";
-    private static final int ASHMEM_SIZE = 464;
-    private static final int HISTORY_SIZE = 20;
+
+    private static final int SAVE_BUFFER = 1;
+    private static final int DELETE_OLD = 2;
+
+    // This isn't static because we need this to happen after registerNativeMethods, however
+    // the class is loaded (and thus static ctor happens) before that occurs.
+    private final int ASHMEM_SIZE = nGetAshmemSize();
+    private final byte[] ZERO_DATA = new byte[ASHMEM_SIZE];
 
     private final Context mContext;
     private final AppOpsManager mAppOps;
+    private final AlarmManager mAlarmManager;
     private final Object mLock = new Object();
     private ArrayList<ActiveBuffer> mActive = new ArrayList<>();
-    private HistoricalData[] mHistoricalLog = new HistoricalData[HISTORY_SIZE];
-    private int mNextHistoricalSlot = 0;
-    private byte[] mTempBuffer = new byte[ASHMEM_SIZE];
+    private File mGraphicsStatsDir;
+    private final Object mFileAccessLock = new Object();
+    private Handler mWriteOutHandler;
+    private boolean mRotateIsScheduled = false;
 
     public GraphicsStatsService(Context context) {
         mContext = context;
         mAppOps = context.getSystemService(AppOpsManager.class);
+        mAlarmManager = context.getSystemService(AlarmManager.class);
+        File systemDataDir = new File(Environment.getDataDirectory(), "system");
+        mGraphicsStatsDir = new File(systemDataDir, "graphicsstats");
+        mGraphicsStatsDir.mkdirs();
+        if (!mGraphicsStatsDir.exists()) {
+            throw new IllegalStateException("Graphics stats directory does not exist: "
+                    + mGraphicsStatsDir.getAbsolutePath());
+        }
+        HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", Process.THREAD_PRIORITY_BACKGROUND);
+        bgthread.start();
+
+        mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() {
+            @Override
+            public boolean handleMessage(Message msg) {
+                switch (msg.what) {
+                    case SAVE_BUFFER:
+                        saveBuffer((HistoricalBuffer) msg.obj);
+                        break;
+                    case DELETE_OLD:
+                        deleteOldBuffers();
+                        break;
+                }
+                return true;
+            }
+        });
+    }
+
+    /**
+     * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because
+     * rotation can be delayed if there's otherwise no activity. However exact is used because
+     * we don't want the system to delay it by TOO much.
+     */
+    private void scheduleRotateLocked() {
+        if (mRotateIsScheduled) {
+            return;
+        }
+        mRotateIsScheduled = true;
+        Calendar calendar = normalizeDate(System.currentTimeMillis());
+        calendar.add(Calendar.DATE, 1);
+        mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm,
+                mWriteOutHandler);
+    }
+
+    private void onAlarm() {
+        synchronized (mLock) {
+            mRotateIsScheduled = false;
+            scheduleRotateLocked();
+            for (ActiveBuffer active : mActive) {
+                try {
+                    active.mCallback.onRotateGraphicsStatsBuffer();
+                } catch (RemoteException e) {
+                    Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers",
+                            active.mInfo.packageName, active.mPid), e);
+                }
+            }
+        }
+        // Give a few seconds for everyone to rotate before doing the cleanup
+        mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000);
     }
 
     @Override
-    public ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token)
+    public ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback token)
             throws RemoteException {
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
@@ -84,9 +162,12 @@
         long callingIdentity = Binder.clearCallingIdentity();
         try {
             mAppOps.checkPackage(uid, packageName);
+            PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
             synchronized (mLock) {
-                pfd = requestBufferForProcessLocked(token, uid, pid, packageName);
+                pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode);
             }
+        } catch (PackageManager.NameNotFoundException ex) {
+            throw new RemoteException("Unable to find package: '" + packageName + "'");
         } finally {
             Binder.restoreCallingIdentity(callingIdentity);
         }
@@ -95,51 +176,130 @@
 
     private ParcelFileDescriptor getPfd(MemoryFile file) {
         try {
+            if (!file.getFileDescriptor().valid()) {
+                throw new IllegalStateException("Invalid file descriptor");
+            }
             return new ParcelFileDescriptor(file.getFileDescriptor());
         } catch (IOException ex) {
             throw new IllegalStateException("Failed to get PFD from memory file", ex);
         }
     }
 
-    private ParcelFileDescriptor requestBufferForProcessLocked(IBinder token,
-            int uid, int pid, String packageName) throws RemoteException {
-        ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName);
+    private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
+            int uid, int pid, String packageName, int versionCode) throws RemoteException {
+        ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
+        scheduleRotateLocked();
         return getPfd(buffer.mProcessBuffer);
     }
 
+    private Calendar normalizeDate(long timestamp) {
+        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        calendar.setTimeInMillis(timestamp);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar;
+    }
+
+    private File pathForApp(BufferInfo info) {
+        String subPath = String.format("%d/%s/%d/total",
+                normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode);
+        return new File(mGraphicsStatsDir, subPath);
+    }
+
+    private void saveBuffer(HistoricalBuffer buffer) {
+        if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName);
+        }
+        synchronized (mFileAccessLock) {
+            File path = pathForApp(buffer.mInfo);
+            File parent = path.getParentFile();
+            parent.mkdirs();
+            if (!parent.exists()) {
+                Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'");
+                return;
+            }
+            nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode,
+                    buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData);
+        }
+        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+    }
+
+    private void deleteRecursiveLocked(File file) {
+        if (file.isDirectory()) {
+            for (File child : file.listFiles()) {
+                deleteRecursiveLocked(child);
+            }
+        }
+        if (!file.delete()) {
+            Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!");
+        }
+    }
+
+    private void deleteOldBuffers() {
+        Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers");
+        synchronized (mFileAccessLock) {
+            File[] files = mGraphicsStatsDir.listFiles();
+            if (files == null || files.length <= 3) {
+                return;
+            }
+            long[] sortedDates = new long[files.length];
+            for (int i = 0; i < files.length; i++) {
+                try {
+                    sortedDates[i] = Long.parseLong(files[i].getName());
+                } catch (NumberFormatException ex) {
+                    // Skip unrecognized folders
+                }
+            }
+            if (sortedDates.length <= 3) {
+                return;
+            }
+            Arrays.sort(sortedDates);
+            for (int i = 0; i < sortedDates.length - 3; i++) {
+                deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i])));
+            }
+        }
+        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+    }
+
+    private void addToSaveQueue(ActiveBuffer buffer) {
+        try {
+            HistoricalBuffer data = new HistoricalBuffer(buffer);
+            Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e);
+        }
+        buffer.closeAllBuffers();
+    }
+
     private void processDied(ActiveBuffer buffer) {
         synchronized (mLock) {
             mActive.remove(buffer);
-            Log.d("GraphicsStats", "Buffer count: " + mActive.size());
         }
-        HistoricalData data = buffer.mPreviousData;
-        buffer.mPreviousData = null;
-        if (data == null) {
-            data = mHistoricalLog[mNextHistoricalSlot];
-            if (data == null) {
-                data = new HistoricalData();
-            }
-        }
-        data.update(buffer.mPackageName, buffer.mUid, buffer.mProcessBuffer);
-        buffer.closeAllBuffers();
-
-        mHistoricalLog[mNextHistoricalSlot] = data;
-        mNextHistoricalSlot = (mNextHistoricalSlot + 1) % mHistoricalLog.length;
+        addToSaveQueue(buffer);
     }
 
-    private ActiveBuffer fetchActiveBuffersLocked(IBinder token, int uid, int pid,
-            String packageName) throws RemoteException {
+    private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
+            String packageName, int versionCode) throws RemoteException {
         int size = mActive.size();
+        long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
         for (int i = 0; i < size; i++) {
-            ActiveBuffer buffers = mActive.get(i);
-            if (buffers.mPid == pid
-                    && buffers.mUid == uid) {
-                return buffers;
+            ActiveBuffer buffer = mActive.get(i);
+            if (buffer.mPid == pid
+                    && buffer.mUid == uid) {
+                // If the buffer is too old we remove it and return a new one
+                if (buffer.mInfo.startTime < today) {
+                    buffer.binderDied();
+                    break;
+                } else {
+                    return buffer;
+                }
             }
         }
         // Didn't find one, need to create it
         try {
-            ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName);
+            ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode);
             mActive.add(buffers);
             return buffers;
         } catch (IOException ex) {
@@ -147,71 +307,106 @@
         }
     }
 
-    private HistoricalData removeHistoricalDataLocked(int uid, String packageName) {
-        for (int i = 0; i < mHistoricalLog.length; i++) {
-            final HistoricalData data = mHistoricalLog[i];
-            if (data != null && data.mUid == uid
-                    && data.mPackageName.equals(packageName)) {
-                if (i == mNextHistoricalSlot) {
-                    mHistoricalLog[i] = null;
-                } else {
-                    mHistoricalLog[i] = mHistoricalLog[mNextHistoricalSlot];
-                    mHistoricalLog[mNextHistoricalSlot] = null;
+    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;
+    }
+
+    private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) {
+        for (File date : mGraphicsStatsDir.listFiles()) {
+            for (File pkg : date.listFiles()) {
+                for (File version : pkg.listFiles()) {
+                    File data = new File(version, "total");
+                    if (skipFiles.contains(data)) {
+                        continue;
+                    }
+                    nAddToDump(dump, data.getAbsolutePath());
                 }
-                return data;
             }
         }
-        return null;
     }
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
+        boolean dumpProto = false;
+        for (String str : args) {
+            if ("--proto".equals(str)) {
+                dumpProto = true;
+                break;
+            }
+        }
+        ArrayList<HistoricalBuffer> buffers;
         synchronized (mLock) {
+            buffers = new ArrayList<>(mActive.size());
             for (int i = 0; i < mActive.size(); i++) {
-                final ActiveBuffer buffer = mActive.get(i);
-                fout.print("Package: ");
-                fout.print(buffer.mPackageName);
-                fout.flush();
                 try {
-                    buffer.mProcessBuffer.readBytes(mTempBuffer, 0, 0, ASHMEM_SIZE);
-                    ThreadedRenderer.dumpProfileData(mTempBuffer, fd);
-                } catch (IOException e) {
-                    fout.println("Failed to dump");
+                    buffers.add(new HistoricalBuffer(mActive.get(i)));
+                } catch (IOException ex) {
+                    // Ignore
                 }
-                fout.println();
             }
-            for (HistoricalData buffer : mHistoricalLog) {
-                if (buffer == null) continue;
-                fout.print("Package: ");
-                fout.print(buffer.mPackageName);
-                fout.flush();
-                ThreadedRenderer.dumpProfileData(buffer.mBuffer, fd);
-                fout.println();
+        }
+        long dump = nCreateDump(fd.getInt$(), dumpProto);
+        try {
+            synchronized (mFileAccessLock) {
+                HashSet<File> skipList = dumpActiveLocked(dump, buffers);
+                buffers.clear();
+                dumpHistoricalLocked(dump, skipList);
             }
+        } finally {
+            nFinishDump(dump);
+        }
+    }
+
+    private static native int nGetAshmemSize();
+    private static native long nCreateDump(int outFd, boolean isProto);
+    private static native void nAddToDump(long dump, String path, String packageName,
+            int versionCode, long startTime, long endTime, byte[] data);
+    private static native void nAddToDump(long dump, String path);
+    private static native void nFinishDump(long dump);
+    private static native void nSaveBuffer(String path, String packageName, int versionCode,
+            long startTime, long endTime, byte[] data);
+
+    private final class BufferInfo {
+        final String packageName;
+        final int versionCode;
+        long startTime;
+        long endTime;
+
+        BufferInfo(String packageName, int versionCode, long startTime) {
+            this.packageName = packageName;
+            this.versionCode = versionCode;
+            this.startTime = startTime;
         }
     }
 
     private final class ActiveBuffer implements DeathRecipient {
+        final BufferInfo mInfo;
         final int mUid;
         final int mPid;
-        final String mPackageName;
+        final IGraphicsStatsCallback mCallback;
         final IBinder mToken;
         MemoryFile mProcessBuffer;
-        HistoricalData mPreviousData;
 
-        ActiveBuffer(IBinder token, int uid, int pid, String packageName)
+        ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
                 throws RemoteException, IOException {
+            mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis());
             mUid = uid;
             mPid = pid;
-            mPackageName = packageName;
-            mToken = token;
+            mCallback = token;
+            mToken = mCallback.asBinder();
             mToken.linkToDeath(this, 0);
-            mProcessBuffer = new MemoryFile("GFXStats-" + uid, ASHMEM_SIZE);
-            mPreviousData = removeHistoricalDataLocked(mUid, mPackageName);
-            if (mPreviousData != null) {
-                mProcessBuffer.writeBytes(mPreviousData.mBuffer, 0, 0, ASHMEM_SIZE);
-            }
+            mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE);
+            mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE);
         }
 
         @Override
@@ -228,17 +423,13 @@
         }
     }
 
-    private final static class HistoricalData {
-        final byte[] mBuffer = new byte[ASHMEM_SIZE];
-        int mUid;
-        String mPackageName;
-
-        void update(String packageName, int uid, MemoryFile file) {
-            mUid = uid;
-            mPackageName = packageName;
-            try {
-                file.readBytes(mBuffer, 0, 0, ASHMEM_SIZE);
-            } catch (IOException e) {}
+    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);
         }
     }
 }
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index 2c3cda5..ac95db5 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -30,6 +30,7 @@
     $(LOCAL_REL_DIR)/com_android_server_UsbHostManager.cpp \
     $(LOCAL_REL_DIR)/com_android_server_VibratorService.cpp \
     $(LOCAL_REL_DIR)/com_android_server_PersistentDataBlockService.cpp \
+    $(LOCAL_REL_DIR)/com_android_server_GraphicsStatsService.cpp \
     $(LOCAL_REL_DIR)/onload.cpp
 
 LOCAL_C_INCLUDES += \
@@ -37,7 +38,6 @@
     external/scrypt/lib/crypto \
     frameworks/base/services \
     frameworks/base/libs \
-    frameworks/base/libs/hwui \
     frameworks/base/core/jni \
     frameworks/native/services \
     system/core/libappfuse/include \
@@ -76,6 +76,7 @@
     libhidltransport \
     libhwbinder \
     libutils \
+    libhwui \
     android.hardware.audio.common@2.0 \
     android.hardware.contexthub@1.0 \
     android.hardware.gnss@1.0 \
diff --git a/services/core/jni/com_android_server_GraphicsStatsService.cpp b/services/core/jni/com_android_server_GraphicsStatsService.cpp
new file mode 100644
index 0000000..5d5728d
--- /dev/null
+++ b/services/core/jni/com_android_server_GraphicsStatsService.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "GraphicsStatsService"
+
+#include <jni.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <ScopedPrimitiveArray.h>
+#include <ScopedUtfChars.h>
+#include <JankTracker.h>
+#include <service/GraphicsStatsService.h>
+
+namespace android {
+
+using namespace android::uirenderer;
+
+static jint getAshmemSize(JNIEnv*, jobject) {
+    return sizeof(ProfileData);
+}
+
+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);
+}
+
+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;
+    LOG_ALWAYS_FATAL_IF(jdata == nullptr && jpath == nullptr, "Path and data can't both be null");
+    if (jdata != nullptr) {
+        ScopedByteArrayRO buffer(env, jdata);
+        LOG_ALWAYS_FATAL_IF(buffer.size() != sizeof(ProfileData),
+                "Buffer size %zu doesn't match expected %zu!", buffer.size(), sizeof(ProfileData));
+        data = reinterpret_cast<const ProfileData*>(buffer.get());
+    }
+    if (jpath != nullptr) {
+        ScopedUtfChars pathChars(env, jpath);
+        LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+        path.assign(pathChars.c_str(), pathChars.size());
+    }
+    ScopedUtfChars packageChars(env, jpackage);
+    LOG_ALWAYS_FATAL_IF(packageChars.size() <= 0 || !packageChars.c_str(), "Failed to get path chars");
+    GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+    LOG_ALWAYS_FATAL_IF(!dump, "null passed for dump pointer");
+
+    const std::string package(packageChars.c_str(), packageChars.size());
+    GraphicsStatsService::addToDump(dump, path, package, versionCode, startTime, endTime, data);
+}
+
+static void addFileToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath) {
+    ScopedUtfChars pathChars(env, jpath);
+    LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+    const std::string path(pathChars.c_str(), pathChars.size());
+    GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+    GraphicsStatsService::addToDump(dump, path);
+}
+
+static void finishDump(JNIEnv*, jobject, jlong dumpPtr) {
+    GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+    GraphicsStatsService::finishDump(dump);
+}
+
+static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jstring jpackage,
+        jint versionCode, jlong startTime, jlong endTime, jbyteArray jdata) {
+    ScopedByteArrayRO buffer(env, jdata);
+    LOG_ALWAYS_FATAL_IF(buffer.size() != sizeof(ProfileData),
+            "Buffer size %zu doesn't match expected %zu!", buffer.size(), sizeof(ProfileData));
+    ScopedUtfChars pathChars(env, jpath);
+    LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+    ScopedUtfChars packageChars(env, jpackage);
+    LOG_ALWAYS_FATAL_IF(packageChars.size() <= 0 || !packageChars.c_str(), "Failed to get path chars");
+
+    const std::string path(pathChars.c_str(), pathChars.size());
+    const std::string package(packageChars.c_str(), packageChars.size());
+    const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer.get());
+    GraphicsStatsService::saveBuffer(path, package, versionCode, startTime, endTime, data);
+}
+
+static const JNINativeMethod sMethods[] = {
+    { "nGetAshmemSize", "()I", (void*) getAshmemSize },
+    { "nCreateDump", "(IZ)J", (void*) createDump },
+    { "nAddToDump", "(JLjava/lang/String;Ljava/lang/String;IJJ[B)V", (void*) addToDump },
+    { "nAddToDump", "(JLjava/lang/String;)V", (void*) addFileToDump },
+    { "nFinishDump", "(J)V", (void*) finishDump },
+    { "nSaveBuffer", "(Ljava/lang/String;Ljava/lang/String;IJJ[B)V", (void*) saveBuffer },
+};
+
+int register_android_server_GraphicsStatsService(JNIEnv* env)
+{
+    return jniRegisterNativeMethods(env, "com/android/server/GraphicsStatsService",
+                                    sMethods, NELEM(sMethods));
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 899640e..f22b330 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -46,6 +46,7 @@
 int register_android_server_Watchdog(JNIEnv* env);
 int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
 int register_android_server_SyntheticPasswordManager(JNIEnv* env);
+int register_android_server_GraphicsStatsService(JNIEnv* env);
 };
 
 using namespace android;
@@ -87,6 +88,7 @@
     register_android_server_HardwarePropertiesManagerService(env);
     register_android_server_storage_AppFuse(env);
     register_android_server_SyntheticPasswordManager(env);
+    register_android_server_GraphicsStatsService(env);
 
     return JNI_VERSION_1_4;
 }