expose hwui frame stats through FrameStatsObserver

Change-Id: I88884bafc8e2f6d7f67a36d3609490e83cf8afd5
diff --git a/core/java/android/view/FrameStatsObserver.java b/core/java/android/view/FrameStatsObserver.java
new file mode 100644
index 0000000..0add607
--- /dev/null
+++ b/core/java/android/view/FrameStatsObserver.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+import android.util.Log;
+import android.os.Looper;
+import android.os.MessageQueue;
+
+import com.android.internal.util.VirtualRefBasePtr;
+
+import java.lang.NullPointerException;
+import java.lang.ref.WeakReference;
+import java.lang.SuppressWarnings;
+
+/**
+ * Provides streaming access to frame stats information from the rendering
+ * subsystem to apps.
+ *
+ * @hide
+ */
+public abstract class FrameStatsObserver {
+    private static final String TAG = "FrameStatsObserver";
+
+    private MessageQueue mMessageQueue;
+    private long[] mBuffer;
+
+    private FrameStats mFrameStats;
+
+    /* package */ ThreadedRenderer mRenderer;
+    /* package */ VirtualRefBasePtr mNative;
+
+    /**
+     * Containing class for frame statistics reported
+     * by the rendering subsystem.
+     */
+    public static class FrameStats {
+        /**
+         * Precise timing data for various milestones in a frame
+         * lifecycle.
+         *
+         * This data is exactly the same as what is returned by
+         * `adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats`
+         *
+         * The fields reported may change from release to release.
+         *
+         * @see {@link http://developer.android.com/training/testing/performance.html}
+         * for a description of the fields present.
+         */
+        public long[] mTimingData;
+    }
+
+    /**
+     * Creates a FrameStatsObserver
+     *
+     * @param looper the looper to use when invoking callbacks
+     */
+    public FrameStatsObserver(@NonNull Looper looper) {
+        if (looper == null) {
+            throw new NullPointerException("looper cannot be null");
+        }
+
+        mMessageQueue = looper.getQueue();
+        if (mMessageQueue == null) {
+            throw new IllegalStateException("invalid looper, null message queue\n");
+        }
+
+        mFrameStats = new FrameStats();
+    }
+
+    /**
+     * Called on provided looper when frame stats data is available
+     * for the previous frame.
+     *
+     * Clients of this class must do as little work as possible within
+     * this callback, as the buffer is shared between the producer and consumer.
+     *
+     * If the consumer is still executing within this method when there is new
+     * data available that data will be dropped. The producer cannot
+     * wait on the consumer.
+     *
+     * @param data the newly available data
+     */
+    public abstract void onDataAvailable(FrameStats data);
+
+    /**
+     * Returns the number of reports dropped as a result of a slow
+     * consumer.
+     */
+    public long getDroppedReportCount() {
+        if (mRenderer == null) {
+            return 0;
+        }
+
+        return mRenderer.getDroppedFrameReportCount();
+    }
+
+    public boolean isRegistered() {
+        return mRenderer != null && mNative != null;
+    }
+
+    // === called by native === //
+    @SuppressWarnings("unused")
+    private void notifyDataAvailable() {
+        mFrameStats.mTimingData = mBuffer;
+        onDataAvailable(mFrameStats);
+    }
+}
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 0e4bc84..78a63a6 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -24,7 +24,9 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -34,12 +36,14 @@
 import android.view.View.AttachInfo;
 
 import com.android.internal.R;
+import com.android.internal.util.VirtualRefBasePtr;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
 
 /**
  * Hardware renderer that proxies the rendering to a render thread. Most calls
@@ -339,6 +343,8 @@
     private boolean mEnabled;
     private boolean mRequested = true;
 
+    private HashSet<FrameStatsObserver> mFrameStatsObservers;
+
     ThreadedRenderer(Context context, boolean translucent) {
         final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
         mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
@@ -947,6 +953,31 @@
         }
     }
 
+    void addFrameStatsObserver(FrameStatsObserver fso) {
+        if (mFrameStatsObservers == null) {
+            mFrameStatsObservers = new HashSet<>();
+        }
+
+        long nativeFso = nAddFrameStatsObserver(mNativeProxy, fso);
+        fso.mRenderer = this;
+        fso.mNative = new VirtualRefBasePtr(nativeFso);
+        mFrameStatsObservers.add(fso);
+    }
+
+    void removeFrameStatsObserver(FrameStatsObserver fso) {
+        if (!mFrameStatsObservers.remove(fso)) {
+            throw new IllegalArgumentException("attempt to remove FrameStatsObserver that was never added");
+        }
+
+        nRemoveFrameStatsObserver(mNativeProxy, fso.mNative.get());
+        fso.mRenderer = null;
+        fso.mNative = null;
+    }
+
+    long getDroppedFrameReportCount() {
+        return nGetDroppedFrameReportCount(mNativeProxy);
+    }
+
     static native void setupShadersDiskCache(String cacheFile);
 
     private static native void nSetAtlas(long nativeProxy, GraphicBuffer buffer, long[] map);
@@ -1000,4 +1031,8 @@
     private static native void nDrawRenderNode(long nativeProxy, long rootRenderNode);
     private static native void nSetContentDrawBounds(long nativeProxy, int left,
              int top, int right, int bottom);
+
+    private static native long nAddFrameStatsObserver(long nativeProxy, FrameStatsObserver fso);
+    private static native void nRemoveFrameStatsObserver(long nativeProxy, long nativeFso);
+    private static native long nGetDroppedFrameReportCount(long nativeProxy);
 }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index b5b0baa..1b8549b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -109,6 +109,7 @@
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
+import java.lang.NullPointerException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
@@ -5380,6 +5381,36 @@
     }
 
     /**
+     * Set an observer to collect stats for each frame rendered for this view.
+     *
+     * @hide
+     */
+    public void addFrameStatsObserver(FrameStatsObserver fso) {
+        if (mAttachInfo != null) {
+            if (mAttachInfo.mHardwareRenderer != null) {
+                mAttachInfo.mHardwareRenderer.addFrameStatsObserver(fso);
+            } else {
+                throw new IllegalStateException("View must be hardware-accelerated");
+            }
+        } else {
+            // TODO: store as pending registration and merge when we are attached to a surface
+            throw new IllegalStateException("View not yet attached");
+        }
+    }
+
+    /**
+     * Remove observer configured to collect frame stats for this view.
+     *
+     * @hide
+     */
+    public void removeFrameStatsObserver(FrameStatsObserver fso) {
+        ThreadedRenderer renderer = getHardwareRenderer();
+        if (renderer != null) {
+            renderer.removeFrameStatsObserver(fso);
+        }
+    }
+
+    /**
      * Call this view's OnClickListener, if it is defined.  Performs all normal
      * actions associated with clicking: reporting accessibility event, playing
      * a sound, etc.
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index dfe0cc7..ee70891 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -34,6 +34,7 @@
 import android.media.session.MediaController;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemProperties;
@@ -794,6 +795,40 @@
         return mCallback;
     }
 
+    /**
+     * Set an observer to collect frame stats for each frame rendererd in this window.
+     *
+     * Must be in hardware rendering mode.
+     * @hide
+     */
+    public final void addFrameStatsObserver(@NonNull FrameStatsObserver fso) {
+        final View decorView = getDecorView();
+        if (decorView == null) {
+            throw new IllegalStateException("can't observe a Window without an attached view");
+        }
+
+        if (fso == null) {
+            throw new NullPointerException("FrameStatsObserver cannot be null");
+        }
+
+        if (fso.isRegistered()) {
+            throw new IllegalStateException("FrameStatsObserver already registered on a Window.");
+        }
+
+        decorView.addFrameStatsObserver(fso);
+    }
+
+    /**
+     * Remove observer and stop listening to frame stats for this window.
+     * @hide
+     */
+    public final void removeFrameStatsObserver(FrameStatsObserver fso) {
+        final View decorView = getDecorView();
+        if (decorView != null) {
+            getDecorView().removeFrameStatsObserver(fso);
+        }
+    }
+
     /** @hide */
     public final void setOnWindowDismissedCallback(OnWindowDismissedCallback dcb) {
         mOnWindowDismissedCallback = dcb;
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 5aa6a73..edced56 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -28,14 +28,18 @@
 #include <EGL/eglext.h>
 #include <EGL/egl_cache.h>
 
+#include <utils/Looper.h>
+#include <utils/RefBase.h>
 #include <utils/StrongPointer.h>
 #include <android_runtime/android_view_Surface.h>
 #include <system/window.h>
 
 #include "android_view_GraphicBuffer.h"
+#include "android_os_MessageQueue.h"
 
 #include <Animator.h>
 #include <AnimationContext.h>
+#include <FrameInfo.h>
 #include <IContextFactory.h>
 #include <JankTracker.h>
 #include <RenderNode.h>
@@ -50,6 +54,12 @@
 using namespace android::uirenderer;
 using namespace android::uirenderer::renderthread;
 
+struct {
+    jfieldID buffer;
+    jfieldID messageQueue;
+    jmethodID notifyData;
+} gFrameStatsObserverClassInfo;
+
 static JNIEnv* getenv(JavaVM* vm) {
     JNIEnv* env;
     if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
@@ -207,6 +217,99 @@
     RootRenderNode* mRootNode;
 };
 
+class ObserverProxy;
+
+class NotifyHandler : public MessageHandler {
+public:
+    NotifyHandler(JavaVM* vm) : mVm(vm) {}
+
+    void setObserver(ObserverProxy* observer) {
+        mObserver = observer;
+    }
+
+    void setBuffer(BufferPool::Buffer* buffer) {
+        mBuffer = buffer;
+    }
+
+    virtual void handleMessage(const Message& message);
+
+private:
+    JavaVM* mVm;
+
+    sp<ObserverProxy> mObserver;
+    BufferPool::Buffer* mBuffer;
+};
+
+class ObserverProxy : public FrameStatsObserver {
+public:
+    ObserverProxy(JavaVM *vm, jobject fso) : mVm(vm) {
+        JNIEnv* env = getenv(mVm);
+
+        jlongArray longArrayLocal = env->NewLongArray(kBufferSize);
+        LOG_ALWAYS_FATAL_IF(longArrayLocal == nullptr,
+                "OOM: can't allocate frame stats buffer");
+        env->SetObjectField(fso, gFrameStatsObserverClassInfo.buffer, longArrayLocal);
+
+        mFsoWeak = env->NewWeakGlobalRef(fso);
+        LOG_ALWAYS_FATAL_IF(mFsoWeak == nullptr,
+                "unable to create frame stats observer reference");
+
+        jobject messageQueueLocal =
+                env->GetObjectField(fso, gFrameStatsObserverClassInfo.messageQueue);
+        mMessageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueLocal);
+        LOG_ALWAYS_FATAL_IF(mMessageQueue == nullptr, "message queue not available");
+
+        mMessageHandler = new NotifyHandler(mVm);
+        LOG_ALWAYS_FATAL_IF(mMessageHandler == nullptr,
+                "OOM: unable to allocate NotifyHandler");
+    }
+
+    ~ObserverProxy() {
+        JNIEnv* env = getenv(mVm);
+        env->DeleteWeakGlobalRef(mFsoWeak);
+    }
+
+    jweak getJavaObjectRef() {
+        return mFsoWeak;
+    }
+
+    virtual void notify(BufferPool::Buffer* buffer) {
+        buffer->incRef();
+        mMessageHandler->setBuffer(buffer);
+        mMessageHandler->setObserver(this);
+        mMessageQueue->getLooper()->sendMessage(mMessageHandler, mMessage);
+    }
+
+private:
+    static const int kBufferSize = static_cast<int>(FrameInfoIndex::NumIndexes);
+
+    JavaVM* mVm;
+    jweak mFsoWeak;
+
+    sp<MessageQueue> mMessageQueue;
+    sp<NotifyHandler> mMessageHandler;
+    Message mMessage;
+};
+
+void NotifyHandler::handleMessage(const Message& message) {
+    JNIEnv* env = getenv(mVm);
+
+    jobject target = env->NewLocalRef(mObserver->getJavaObjectRef());
+
+    if (target != nullptr) {
+        jobject javaBuffer = env->GetObjectField(target, gFrameStatsObserverClassInfo.buffer);
+        if (javaBuffer != nullptr) {
+            env->SetLongArrayRegion(reinterpret_cast<jlongArray>(javaBuffer),
+                    0, mBuffer->getSize(), mBuffer->getBuffer());
+            env->CallVoidMethod(target, gFrameStatsObserverClassInfo.notifyData);
+            env->DeleteLocalRef(target);
+        }
+    }
+
+    mBuffer->release();
+    mObserver.clear();
+}
+
 static void android_view_ThreadedRenderer_setAtlas(JNIEnv* env, jobject clazz,
         jlong proxyPtr, jobject graphicBuffer, jlongArray atlasMapArray) {
     sp<GraphicBuffer> buffer = graphicBufferForJavaObject(env, graphicBuffer);
@@ -468,6 +571,42 @@
 }
 
 // ----------------------------------------------------------------------------
+// FrameStatsObserver
+// ----------------------------------------------------------------------------
+
+static jlong android_view_ThreadedRenderer_addFrameStatsObserver(JNIEnv* env,
+        jclass clazz, jlong proxyPtr, jobject fso) {
+    JavaVM* vm = nullptr;
+    if (env->GetJavaVM(&vm) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Unable to get Java VM");
+        return 0;
+    }
+
+    renderthread::RenderProxy* renderProxy =
+            reinterpret_cast<renderthread::RenderProxy*>(proxyPtr);
+
+    FrameStatsObserver* observer = new ObserverProxy(vm, fso);
+    renderProxy->addFrameStatsObserver(observer);
+    return reinterpret_cast<jlong>(observer);
+}
+
+static void android_view_ThreadedRenderer_removeFrameStatsObserver(JNIEnv* env, jclass clazz,
+        jlong proxyPtr, jlong observerPtr) {
+    FrameStatsObserver* observer = reinterpret_cast<FrameStatsObserver*>(observerPtr);
+    renderthread::RenderProxy* renderProxy =
+            reinterpret_cast<renderthread::RenderProxy*>(proxyPtr);
+
+    renderProxy->removeFrameStatsObserver(observer);
+}
+
+static jint android_view_ThreadedRenderer_getDroppedFrameReportCount(JNIEnv* env, jclass clazz,
+        jlong proxyPtr) {
+    renderthread::RenderProxy* renderProxy =
+            reinterpret_cast<renderthread::RenderProxy*>(proxyPtr);
+    return renderProxy->getDroppedFrameReportCount();
+}
+
+// ----------------------------------------------------------------------------
 // Shaders
 // ----------------------------------------------------------------------------
 
@@ -523,9 +662,26 @@
     { "nRemoveRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_removeRenderNode},
     { "nDrawRenderNode", "(JJ)V", (void*) android_view_ThreadedRendererd_drawRenderNode},
     { "nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds},
+    { "nAddFrameStatsObserver",
+            "(JLandroid/view/FrameStatsObserver;)J",
+            (void*)android_view_ThreadedRenderer_addFrameStatsObserver },
+    { "nRemoveFrameStatsObserver",
+            "(JJ)V",
+            (void*)android_view_ThreadedRenderer_removeFrameStatsObserver },
+    { "nGetDroppedFrameReportCount",
+            "(J)J",
+            (void*)android_view_ThreadedRenderer_getDroppedFrameReportCount },
 };
 
 int register_android_view_ThreadedRenderer(JNIEnv* env) {
+    jclass clazz = FindClassOrDie(env, "android/view/FrameStatsObserver");
+    gFrameStatsObserverClassInfo.messageQueue  =
+            GetFieldIDOrDie(env, clazz, "mMessageQueue", "Landroid/os/MessageQueue;");
+    gFrameStatsObserverClassInfo.buffer =
+            GetFieldIDOrDie(env, clazz, "mBuffer", "[J");
+    gFrameStatsObserverClassInfo.notifyData =
+            GetMethodIDOrDie(env, clazz, "notifyDataAvailable", "()V");
+
     return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
 }
 
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 8ba6318..483ccf7 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -235,7 +235,8 @@
     tests/unit/LinearAllocatorTests.cpp \
     tests/unit/VectorDrawableTests.cpp \
     tests/unit/OffscreenBufferPoolTests.cpp \
-    tests/unit/StringUtilsTests.cpp
+    tests/unit/StringUtilsTests.cpp \
+    tests/unit/BufferPoolTests.cpp
 
 ifeq (true, $(HWUI_NEW_OPS))
     LOCAL_SRC_FILES += \
diff --git a/libs/hwui/BufferPool.h b/libs/hwui/BufferPool.h
new file mode 100644
index 0000000..9bda233
--- /dev/null
+++ b/libs/hwui/BufferPool.h
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <utils/RefBase.h>
+#include <utils/Log.h>
+
+#include <atomic>
+#include <stdint.h>
+#include <memory>
+#include <mutex>
+
+namespace android {
+namespace uirenderer {
+
+/*
+ * Simple thread-safe pool of int64_t arrays of a provided size.
+ *
+ * Permits allocating a client-provided max number of buffers.
+ * If all buffers are in use, refuses to service any more
+ * acquire requests until buffers are re-released to the pool.
+ */
+class BufferPool : public VirtualLightRefBase {
+public:
+    class Buffer {
+    public:
+        int64_t* getBuffer() { return mBuffer.get(); }
+        size_t getSize() { return mSize; }
+
+        void release() {
+            LOG_ALWAYS_FATAL_IF(mPool.get() == nullptr, "attempt to release unacquired buffer");
+            mPool->release(this);
+        }
+
+        Buffer* incRef() {
+            mRefs++;
+            return this;
+        }
+
+        int decRef() {
+            int refs = mRefs.fetch_sub(1);
+            LOG_ALWAYS_FATAL_IF(refs == 0, "buffer reference decremented below 0");
+            return refs - 1;
+        }
+
+    private:
+        friend class BufferPool;
+
+        Buffer(BufferPool* pool, size_t size) {
+            mSize = size;
+            mBuffer.reset(new int64_t[size]);
+            mPool = pool;
+            mRefs++;
+        }
+
+        void setPool(BufferPool* pool) {
+            mPool = pool;
+        }
+
+        std::unique_ptr<Buffer> mNext;
+        std::unique_ptr<int64_t[]> mBuffer;
+        sp<BufferPool> mPool;
+        size_t mSize;
+
+        std::atomic_int mRefs;
+    };
+
+    BufferPool(size_t bufferSize, size_t count)
+            : mBufferSize(bufferSize), mCount(count) {}
+
+    /**
+     * Acquires a buffer from the buffer pool if available.
+     *
+     * Only `mCount` buffers are allowed to be in use at a single
+     * instance.
+     *
+     * If no buffer is available, i.e. `mCount` buffers are in use,
+     * returns nullptr.
+     *
+     * The pointer returned from this method *MUST NOT* be freed, instead
+     * BufferPool::release() must be called upon it when the client
+     * is done with it. Failing to release buffers will eventually make the
+     * BufferPool refuse to service any more BufferPool::acquire() requests.
+     */
+    BufferPool::Buffer* acquire() {
+        std::lock_guard<std::mutex> lock(mLock);
+
+        if (mHead.get() != nullptr) {
+            BufferPool::Buffer* res = mHead.release();
+            mHead = std::move(res->mNext);
+            res->mNext.reset(nullptr);
+            res->setPool(this);
+            res->incRef();
+            return res;
+        }
+
+        if (mAllocatedCount < mCount) {
+            ++mAllocatedCount;
+            return new BufferPool::Buffer(this, mBufferSize);
+        }
+
+        return nullptr;
+    }
+
+    /**
+     * Releases a buffer previously acquired by BufferPool::acquire().
+     *
+     * The released buffer is not valid after calling this method and
+     * attempting to use will result in undefined behavior.
+     */
+    void release(BufferPool::Buffer* buffer) {
+        std::lock_guard<std::mutex> lock(mLock);
+
+        if (buffer->decRef() != 0) {
+            return;
+        }
+
+        buffer->setPool(nullptr);
+
+        BufferPool::Buffer* list = mHead.get();
+        if (list == nullptr) {
+            mHead.reset(buffer);
+            mHead->mNext.reset(nullptr);
+            return;
+        }
+
+        while (list->mNext.get() != nullptr) {
+            list = list->mNext.get();
+        }
+
+        list->mNext.reset(buffer);
+    }
+
+    /*
+     * Used for testing.
+     */
+    size_t getAvailableBufferCount() {
+        size_t remainingToAllocateCount = mCount - mAllocatedCount;
+
+        BufferPool::Buffer* list = mHead.get();
+        if (list == nullptr) return remainingToAllocateCount;
+
+        int count = 1;
+        while (list->mNext.get() != nullptr) {
+            count++;
+            list = list->mNext.get();
+        }
+
+        return count + remainingToAllocateCount;
+    }
+
+private:
+    mutable std::mutex mLock;
+
+    size_t mBufferSize;
+    size_t mCount;
+    size_t mAllocatedCount = 0;
+    std::unique_ptr<BufferPool::Buffer> mHead;
+};
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h
index f8013ab..0baca39 100644
--- a/libs/hwui/FrameInfo.h
+++ b/libs/hwui/FrameInfo.h
@@ -118,6 +118,10 @@
         set(FrameInfoIndex::Flags) |= static_cast<uint64_t>(frameInfoFlag);
     }
 
+    const int64_t* data() const {
+        return mFrameInfo;
+    }
+
     inline int64_t operator[](FrameInfoIndex index) const {
         return get(index);
     }
diff --git a/libs/hwui/FrameStatsObserver.h b/libs/hwui/FrameStatsObserver.h
new file mode 100644
index 0000000..7abc9f1
--- /dev/null
+++ b/libs/hwui/FrameStatsObserver.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <utils/RefBase.h>
+
+#include "BufferPool.h"
+
+namespace android {
+namespace uirenderer {
+
+class FrameStatsObserver : public VirtualLightRefBase {
+public:
+    virtual void notify(BufferPool::Buffer* buffer);
+};
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/FrameStatsReporter.h b/libs/hwui/FrameStatsReporter.h
new file mode 100644
index 0000000..b8a9432
--- /dev/null
+++ b/libs/hwui/FrameStatsReporter.h
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <utils/RefBase.h>
+#include <utils/Log.h>
+
+#include "BufferPool.h"
+#include "FrameInfo.h"
+#include "FrameStatsObserver.h"
+
+#include <string.h>
+#include <vector>
+
+namespace android {
+namespace uirenderer {
+
+class FrameStatsReporter {
+public:
+    FrameStatsReporter() {
+        mBufferPool = new BufferPool(kBufferSize, kBufferCount);
+        LOG_ALWAYS_FATAL_IF(mBufferPool.get() == nullptr, "OOM: unable to allocate buffer pool");
+    }
+
+    void addObserver(FrameStatsObserver* observer) {
+        mObservers.push_back(observer);
+    }
+
+    bool removeObserver(FrameStatsObserver* observer) {
+        for (size_t i = 0; i < mObservers.size(); i++) {
+            if (mObservers[i].get() == observer) {
+                mObservers.erase(mObservers.begin() + i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    bool hasObservers() {
+        return mObservers.size() > 0;
+    }
+
+    void reportFrameStats(const int64_t* stats) {
+        BufferPool::Buffer* statsBuffer = mBufferPool->acquire();
+
+        if (statsBuffer != nullptr) {
+            // copy in frame stats
+            memcpy(statsBuffer->getBuffer(), stats, kBufferSize * sizeof(*stats));
+
+            // notify on requested threads
+            for (size_t i = 0; i < mObservers.size(); i++) {
+                mObservers[i]->notify(statsBuffer);
+            }
+
+            // drop our reference
+            statsBuffer->release();
+        } else {
+            mDroppedReports++;
+        }
+    }
+
+    int getDroppedReports() { return mDroppedReports; }
+
+private:
+    static const size_t kBufferCount = 3;
+    static const size_t kBufferSize = static_cast<size_t>(FrameInfoIndex::NumIndexes);
+
+    std::vector< sp<FrameStatsObserver> > mObservers;
+
+    sp<BufferPool> mBufferPool;
+
+    int mDroppedReports = 0;
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 6f8d627..cdd2da0 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -505,6 +505,9 @@
 
     mJankTracker.addFrame(*mCurrentFrameInfo);
     mRenderThread.jankTracker().addFrame(*mCurrentFrameInfo);
+    if (CC_UNLIKELY(mFrameStatsReporter.get() != nullptr)) {
+        mFrameStatsReporter->reportFrameStats(mCurrentFrameInfo->data());
+    }
 
     GpuMemoryTracker::onFrameCompleted();
 }
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 8e64cbb..270fb1f 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -20,6 +20,7 @@
 #include "DamageAccumulator.h"
 #include "FrameInfo.h"
 #include "FrameInfoVisualizer.h"
+#include "FrameStatsReporter.h"
 #include "IContextFactory.h"
 #include "LayerUpdateQueue.h"
 #include "RenderNode.h"
@@ -139,6 +140,31 @@
         return mRenderThread.renderState();
     }
 
+    void addFrameStatsObserver(FrameStatsObserver* observer) {
+        if (mFrameStatsReporter.get() == nullptr) {
+            mFrameStatsReporter.reset(new FrameStatsReporter());
+        }
+
+        mFrameStatsReporter->addObserver(observer);
+    }
+
+    void removeFrameStatsObserver(FrameStatsObserver* observer) {
+        if (mFrameStatsReporter.get() != nullptr) {
+            mFrameStatsReporter->removeObserver(observer);
+            if (!mFrameStatsReporter->hasObservers()) {
+                mFrameStatsReporter.reset(nullptr);
+            }
+        }
+    }
+
+    long getDroppedFrameReportCount() {
+        if (mFrameStatsReporter.get() != nullptr) {
+            return mFrameStatsReporter->getDroppedReports();
+        }
+
+        return 0;
+    }
+
 private:
     friend class RegisterFrameCallbackTask;
     // TODO: Replace with something better for layer & other GL object
@@ -187,6 +213,7 @@
     std::string mName;
     JankTracker mJankTracker;
     FrameInfoVisualizer mProfiler;
+    std::unique_ptr<FrameStatsReporter> mFrameStatsReporter;
 
     std::set<RenderNode*> mPrefetechedLayers;
 
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index db2a2c8..1d1b144 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -568,6 +568,54 @@
     post(task);
 }
 
+CREATE_BRIDGE2(addFrameStatsObserver, CanvasContext* context,
+        FrameStatsObserver* frameStatsObserver) {
+   args->context->addFrameStatsObserver(args->frameStatsObserver);
+   if (args->frameStatsObserver != nullptr) {
+       args->frameStatsObserver->decStrong(args->context);
+   }
+   return nullptr;
+}
+
+void RenderProxy::addFrameStatsObserver(FrameStatsObserver* observer) {
+    SETUP_TASK(addFrameStatsObserver);
+    args->context = mContext;
+    args->frameStatsObserver = observer;
+    if (observer != nullptr) {
+        observer->incStrong(mContext);
+    }
+    post(task);
+}
+
+CREATE_BRIDGE2(removeFrameStatsObserver, CanvasContext* context,
+        FrameStatsObserver* frameStatsObserver) {
+   args->context->removeFrameStatsObserver(args->frameStatsObserver);
+   if (args->frameStatsObserver != nullptr) {
+       args->frameStatsObserver->decStrong(args->context);
+   }
+   return nullptr;
+}
+
+void RenderProxy::removeFrameStatsObserver(FrameStatsObserver* observer) {
+    SETUP_TASK(removeFrameStatsObserver);
+    args->context = mContext;
+    args->frameStatsObserver = observer;
+    if (observer != nullptr) {
+        observer->incStrong(mContext);
+    }
+    post(task);
+}
+
+CREATE_BRIDGE1(getDroppedFrameReportCount, CanvasContext* context) {
+    return (void*) args->context->getDroppedFrameReportCount();
+}
+
+long RenderProxy::getDroppedFrameReportCount() {
+    SETUP_TASK(getDroppedFrameReportCount);
+    args->context = mContext;
+    return (long) postAndWait(task);
+}
+
 void RenderProxy::post(RenderTask* task) {
     mRenderThread.queue(task);
 }
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 0f91b2a..4180d802 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -29,6 +29,7 @@
 #include <utils/StrongPointer.h>
 
 #include "../Caches.h"
+#include "../FrameStatsObserver.h"
 #include "../IContextFactory.h"
 #include "CanvasContext.h"
 #include "DrawFrameTask.h"
@@ -112,6 +113,10 @@
     ANDROID_API void drawRenderNode(RenderNode* node);
     ANDROID_API void setContentDrawBounds(int left, int top, int right, int bottom);
 
+    ANDROID_API void addFrameStatsObserver(FrameStatsObserver* observer);
+    ANDROID_API void removeFrameStatsObserver(FrameStatsObserver* observer);
+    ANDROID_API long getDroppedFrameReportCount();
+
 private:
     RenderThread& mRenderThread;
     CanvasContext* mContext;
diff --git a/libs/hwui/tests/unit/BufferPoolTests.cpp b/libs/hwui/tests/unit/BufferPoolTests.cpp
new file mode 100644
index 0000000..09bd302
--- /dev/null
+++ b/libs/hwui/tests/unit/BufferPoolTests.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 <BufferPool.h>
+#include <utils/StrongPointer.h>
+
+namespace android {
+namespace uirenderer {
+
+TEST(BufferPool, acquireThenRelease) {
+    static const int numRuns = 5;
+
+    // 10 buffers of size 1
+    static const size_t bufferSize = 1;
+    static const size_t bufferCount = 10;
+    sp<BufferPool> pool = new BufferPool(bufferSize, bufferCount);
+
+    for (int run = 0; run < numRuns; run++) {
+        BufferPool::Buffer* acquiredBuffers[bufferCount];
+        for (size_t i = 0; i < bufferCount; i++) {
+            ASSERT_EQ(bufferCount - i, pool->getAvailableBufferCount());
+            acquiredBuffers[i] = pool->acquire();
+            ASSERT_NE(nullptr, acquiredBuffers[i]);
+        }
+
+        for (size_t i = 0; i < bufferCount; i++) {
+            ASSERT_EQ(i, pool->getAvailableBufferCount());
+            acquiredBuffers[i]->release();
+            acquiredBuffers[i] = nullptr;
+        }
+
+        ASSERT_EQ(bufferCount, pool->getAvailableBufferCount());
+    }
+}
+
+TEST(BufferPool, acquireReleaseInterleaved) {
+    static const int numRuns = 5;
+
+    // 10 buffers of size 1
+    static const size_t bufferSize = 1;
+    static const size_t bufferCount = 10;
+
+    sp<BufferPool> pool = new BufferPool(bufferSize, bufferCount);
+
+    for (int run = 0; run < numRuns; run++) {
+        BufferPool::Buffer* acquiredBuffers[bufferCount];
+
+        // acquire all
+        for (size_t i = 0; i < bufferCount; i++) {
+            ASSERT_EQ(bufferCount - i, pool->getAvailableBufferCount());
+            acquiredBuffers[i] = pool->acquire();
+            ASSERT_NE(nullptr, acquiredBuffers[i]);
+        }
+
+        // release half
+        for (size_t i = 0; i < bufferCount / 2; i++) {
+            ASSERT_EQ(i, pool->getAvailableBufferCount());
+            acquiredBuffers[i]->release();
+            acquiredBuffers[i] = nullptr;
+        }
+
+        const size_t expectedRemaining = bufferCount / 2;
+        ASSERT_EQ(expectedRemaining, pool->getAvailableBufferCount());
+
+        // acquire half
+        for (size_t i = 0; i < bufferCount / 2; i++) {
+            ASSERT_EQ(expectedRemaining - i, pool->getAvailableBufferCount());
+            acquiredBuffers[i] = pool->acquire();
+        }
+
+        // acquire one more, should fail
+        ASSERT_EQ(nullptr, pool->acquire());
+
+        // release all
+        for (size_t i = 0; i < bufferCount; i++) {
+            ASSERT_EQ(i, pool->getAvailableBufferCount());
+            acquiredBuffers[i]->release();
+            acquiredBuffers[i] = nullptr;
+        }
+
+        ASSERT_EQ(bufferCount, pool->getAvailableBufferCount());
+    }
+}
+
+};
+};