swappy: add stats based on ANDROID_get_frame_timestamps

Add a feature to Swappy to collect frame statistics.

Test: logcat -s FrameStatistics
Bug: 122345413
Change-Id: I4de420ab6ce0c8ac35882c04cecf0e243cb6c701
diff --git a/include/swappy/swappy_extra.h b/include/swappy/swappy_extra.h
index bb3e5c8..05dadcb 100644
--- a/include/swappy/swappy_extra.h
+++ b/include/swappy/swappy_extra.h
@@ -21,13 +21,17 @@
 #include <EGL/eglext.h>
 #include <jni.h>
 
+#define MAX_FRAME_BUCKETS 6
+
+
 #ifdef __cplusplus
 extern "C" {
 #endif
 
-// If app is using choreographer and wants to provide choreographer ticks to swappy,
-// call the function below. This function must be called before the first Swappy_swap() call
-// for the first time. Afterwards, call this every choreographer tick
+// If an app wishes to use the Android choreographer to provide ticks to Swappy, it can
+// call the function below.
+// This function *must* be called before the first Swappy_swap() call.
+// Afterwards, call this function every choreographer tick.
 void Swappy_onChoreographer(int64_t frameTimeNanos);
 
 // Pass callbacks to be called each frame to trace execution
@@ -43,16 +47,58 @@
 };
 void Swappy_injectTracer(const SwappyTracer *t);
 
-// toggle auto swap interval detection on/off
-// by default, swappy will adjust the swap interval based on actual frame rendering time.
-// App can set this mode to off using this function.
-// If App wants to override the swap interval calculated by swappy it can again to
-// Swappy_setSwapIntervalNS. Swappy will set swap interval to the overridden value and
-// reset its frame timings.
-// NOTE: Swappy may still change this value based on new frames rendering time. To completely
-// override auto swap interval value app needs to call first to Swappy_setAutoSwapInterval(false);
+// Toggle auto-swap interval detection on/off
+// By default, Swappy will adjust the swap interval based on actual frame rendering time.
+// If an app wants to override the swap interval calculated by Swappy, it can call
+// Swappy_setSwapIntervalNS. This will temporarily override Swappy's frame timings but, unless
+// Swappy_setAutoSwapInterval(false) is called, the timings will continue to be be updated
+// dynamically, so the swap interval may change.
 void Swappy_setAutoSwapInterval(bool enabled);
 
+// Toggle statistics collection on/off
+// By default, stats collection is off and there is no overhead related to stats.
+// An app can turn on stats collection by calling Swappy_setStatsMode(true).
+// Then, the app is expected to call Swappy_recordFrameStart for each frame before starting to
+// do any CPU related work.
+// Stats will be logged to logcat with a 'FrameStatistics' tag.
+// An app can get the stats by calling Swappy_getStats.
+void Swappy_enableStats(bool enabled);
+
+struct Swappy_Stats {
+    // total frames swapped by swappy
+    uint64_t totalFrames;
+
+    // Histogram of the number of screen refreshes a frame waited in the compositor queue after
+    // rendering was completed.
+    // for example:
+    //     if a frame waited 2 refresh periods in the compositor queue after rendering was done,
+    //     the frame will be counted in idleFrames[2]
+    uint64_t idleFrames[MAX_FRAME_BUCKETS];
+
+    // Histogram of the number of screen refreshes passed between the requested presentation time
+    // and the actual present time.
+    // for example:
+    //     if a frame was presented 2 refresh periods after the requested timestamp swappy set,
+    //     the frame will be counted in lateFrames[2]
+    uint64_t lateFrames[MAX_FRAME_BUCKETS];
+
+    // Histogram of the number of screen refreshes passed between two consecutive frames
+    // for example:
+    //     if frame N was presented 2 refresh periods after frame N-1
+    //     frame N will be counted in offsetFromPreviousFrame[2]
+    uint64_t offsetFromPreviousFrame[MAX_FRAME_BUCKETS];
+
+    // Histogram of the number of screen refreshes passed between the call to
+    // Swappy_recordFrameStart and the actual present time.
+    //     if a frame was presented 2 refresh periods after the call to Swappy_recordFrameStart
+    //     the frame will be counted in latencyFrames[2]
+    uint64_t latencyFrames[MAX_FRAME_BUCKETS];
+};
+
+void Swappy_recordFrameStart(EGLDisplay display, EGLSurface surface);
+
+void Swappy_getStats(Swappy_Stats *);
+
 #ifdef __cplusplus
 };
 #endif
diff --git a/samples/bouncyball/app/src/main/cpp/Orbit.cpp b/samples/bouncyball/app/src/main/cpp/Orbit.cpp
index 799c6ef..d9a710d 100644
--- a/samples/bouncyball/app/src/main/cpp/Orbit.cpp
+++ b/samples/bouncyball/app/src/main/cpp/Orbit.cpp
@@ -16,6 +16,7 @@
 
 #define LOG_TAG "Orbit"
 
+#include <cmath>
 #include <string>
 
 #include <jni.h>
@@ -122,4 +123,49 @@
     Renderer::getInstance()->setWorkload(load);
 }
 
+JNIEXPORT int JNICALL
+Java_com_prefabulated_bouncyball_OrbitActivity_nGetSwappyStats(JNIEnv * /* env */,
+                                                               jobject /* this */,
+                                                               jint stat,
+                                                               jint bin) {
+    static bool enabled = false;
+    if (!enabled) {
+        Swappy_enableStats(true);
+        enabled = true;
+    }
+
+    // stats are read one by one, query once per stat
+    static Swappy_Stats stats;
+    static int stat_idx = -1;
+
+    if (stat_idx != stat) {
+        Swappy_getStats(&stats);
+        stat_idx = stat;
+    }
+
+    int value = 0;
+
+    if (stats.totalFrames) {
+        switch (stat) {
+            case 0:
+                value = stats.idleFrames[bin];
+                break;
+            case 1:
+                value = stats.lateFrames[bin];
+                break;
+            case 2:
+                value = stats.offsetFromPreviousFrame[bin];
+                break;
+            case 3:
+                value = stats.latencyFrames[bin];
+                break;
+            default:
+                return stats.totalFrames;
+        }
+        value = std::round(value * 100.0f / stats.totalFrames);
+    }
+
+    return value;
+}
+
 } // extern "C"
\ No newline at end of file
diff --git a/samples/bouncyball/app/src/main/cpp/Renderer.cpp b/samples/bouncyball/app/src/main/cpp/Renderer.cpp
index e3bc21f..c0c4c1e 100644
--- a/samples/bouncyball/app/src/main/cpp/Renderer.cpp
+++ b/samples/bouncyball/app/src/main/cpp/Renderer.cpp
@@ -214,6 +214,8 @@
         return;
     }
 
+    Swappy_recordFrameStart(threadState->display, threadState->surface);
+
     calculateFps();
 
     float deltaSeconds = threadState->swapIntervalNS / 1e9f;
diff --git a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
index 56e0d42..aa663f5 100644
--- a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
+++ b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
@@ -134,6 +134,67 @@
         }
     }
 
+    private void buildSwappyStatsGrid() {
+        GridLayout infoGrid = findViewById(R.id.swappy_stats_grid);
+
+        // Add the header row
+        GridLayout.Spec headerRowSpec = GridLayout.spec(0);
+        for (int column = 0; column < mSwappyGrid[0].length; ++column) {
+            AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
+            GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
+            cell.setLayoutParams(new GridLayout.LayoutParams(headerRowSpec, colSpec));
+            configureGridCell(cell);
+
+            if (column == 0) {
+                cell.setText("");
+            } else {
+                cell.setText(String.format(Locale.US, "%d", column - 1));
+            }
+            infoGrid.addView(cell);
+            mSwappyGrid[0][column] = cell;
+        }
+
+        // Add the data rows
+        for (int row = 1; row < mSwappyGrid.length; ++row) {
+            GridLayout.Spec rowSpec = GridLayout.spec(row);
+
+            for (int column = 0; column < mSwappyGrid[row].length; ++column) {
+                AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
+                GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
+                cell.setLayoutParams(new GridLayout.LayoutParams(rowSpec, colSpec));
+                cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
+                configureGridCell(cell);
+
+                if (column == 0) {
+                    switch (row) {
+                        case 1:
+                            cell.setText(R.string.idle_frames);
+                            break;
+                        case 2:
+                            cell.setText(R.string.late_frames);
+                            break;
+                        case 3:
+                            cell.setText(R.string.offset_frames);
+                            break;
+                        case 4:
+                            cell.setText(R.string.latency_frames);
+                            break;
+                    }
+                } else {
+                    cell.setText("0%");
+                }
+                infoGrid.addView(cell);
+                mSwappyGrid[row][column] = cell;
+            }
+        }
+
+        for (TextView[] row : mSwappyGrid) {
+            for (TextView column : row) {
+                column.setWidth(infoGrid.getWidth() / infoGrid.getColumnCount());
+            }
+        }
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -158,6 +219,7 @@
         mInfoOverlay.setBackgroundColor(0x80000000);
 
         buildChoreographerInfoGrid();
+        buildSwappyStatsGrid();
 
         TextView appOffsetView = findViewById(R.id.app_offset);
         appOffsetView.setText(String.format(Locale.US, "App Offset: %.1f ms", appVsyncOffsetNanos / (float) ONE_MS_IN_NS));
@@ -273,6 +335,15 @@
         mChoreographerInfoGrid[row][bin + 1].setText(String.valueOf(value));
     }
 
+    private void updateSwappyStatsBin(int row, int bin, int value) {
+        if (value == mLastSwappyStatsValues[row - 1][bin]) {
+            return;
+        }
+
+        mLastSwappyStatsValues[row - 1][bin] = value;
+        mSwappyGrid[row][bin + 1].setText(String.valueOf(value) + "%");
+    }
+
     private void dumpBins() {
         Trace.beginSection("dumpBins");
 
@@ -308,6 +379,16 @@
         }
         Trace.endSection();
 
+        Trace.beginSection("updateSwappyStatsGrid");
+        for (int stat = 0; stat < 4; ++stat) {
+            for (int bin = 0; bin < SWAPPY_STATS_BIN_COUNT; ++bin) {
+                updateSwappyStatsBin(stat +1, bin, nGetSwappyStats(stat, bin));
+            }
+        }
+        TextView appOffsetView = findViewById(R.id.swappy_stats);
+        appOffsetView.setText(String.format(Locale.US, "SwappyStats: %d Total Frames", nGetSwappyStats(-1, 0)));
+        Trace.endSection();
+
         Trace.beginSection("clearSecondBins");
         for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
             mArrivalBinsLastSecond[bin] = 0;
@@ -375,6 +456,7 @@
     public native void nSetWorkload(int load);
     public native void nSetAutoSwapInterval(boolean enabled);
     public native float nGetAverageFps();
+    public native int nGetSwappyStats(int stat, int bin);
 
     private MenuItem mInfoOverlayButton;
 
@@ -382,6 +464,9 @@
     private final TextView[][] mChoreographerInfoGrid = new TextView[5][7];
     private final int[][] mLastChoreographerInfoValues = new int[4][6];
 
+    private final TextView[][] mSwappyGrid = new TextView[5][7];
+    private final int[][] mLastSwappyStatsValues = new int[4][6];
+
     private boolean mIsRunning;
     private boolean mInfoOverlayEnabled = false;
 
@@ -389,6 +474,7 @@
     private final Handler mUIThreadHandler = new Handler(Looper.getMainLooper());
 
     private static final int CHOREOGRAPHER_INFO_BIN_COUNT = 6;
+    private static final int SWAPPY_STATS_BIN_COUNT = 6;
     private long mLastDumpTime;
     private long mLastArrivalTime;
     private long mLastFrameTimestamp;
diff --git a/samples/bouncyball/app/src/main/res/layout/activity_orbit.xml b/samples/bouncyball/app/src/main/res/layout/activity_orbit.xml
index 1540b3f..f87ae46 100644
--- a/samples/bouncyball/app/src/main/res/layout/activity_orbit.xml
+++ b/samples/bouncyball/app/src/main/res/layout/activity_orbit.xml
@@ -61,6 +61,23 @@
             android:id="@+id/choreographer_info_grid"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:layout_marginBottom="10dp"
+            android:columnCount="7">
+
+        </GridLayout>
+
+        <TextView
+            android:id="@+id/swappy_stats"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="6dp"
+            android:text="@string/swappy_stats"
+            android:textAppearance="@style/InfoTextMedium" />
+
+        <GridLayout
+            android:id="@+id/swappy_stats_grid"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
             android:columnCount="7">
 
         </GridLayout>
diff --git a/samples/bouncyball/app/src/main/res/values/strings.xml b/samples/bouncyball/app/src/main/res/values/strings.xml
index e965c6a..984d435 100644
--- a/samples/bouncyball/app/src/main/res/values/strings.xml
+++ b/samples/bouncyball/app/src/main/res/values/strings.xml
@@ -10,6 +10,11 @@
     <string name="info_overlay">Info Overlay</string>
     <string name="settings">Settings</string>
     <string name="sf_offset">SF Offset:</string>
+    <string name="swappy_stats">Swappy Stats</string>
+    <string name="idle_frames">Idle</string>
+    <string name="late_frames">Late</string>
+    <string name="offset_frames">Offset</string>
+    <string name="latency_frames">Latency</string>
 
     <!-- Preference strings -->
 
diff --git a/src/swappy/CMakeLists.txt b/src/swappy/CMakeLists.txt
index 4d4dadf..46ff2fb 100644
--- a/src/swappy/CMakeLists.txt
+++ b/src/swappy/CMakeLists.txt
@@ -22,6 +22,7 @@
              ${SOURCE_LOCATION}/ChoreographerFilter.cpp
              ${SOURCE_LOCATION}/ChoreographerThread.cpp
              ${SOURCE_LOCATION}/EGL.cpp
+             ${SOURCE_LOCATION}/FrameStatistics.cpp
              ${SOURCE_LOCATION}/CpuInfo.cpp
              ${SOURCE_LOCATION}/Swappy.cpp
              ${SOURCE_LOCATION}/Settings.cpp
diff --git a/src/swappy/src/main/cpp/EGL.cpp b/src/swappy/src/main/cpp/EGL.cpp
index c8d241a..1666492 100644
--- a/src/swappy/src/main/cpp/EGL.cpp
+++ b/src/swappy/src/main/cpp/EGL.cpp
@@ -16,6 +16,8 @@
 
 #include "EGL.h"
 
+#include <vector>
+
 #define LOG_TAG "Swappy::EGL"
 
 #include "Log.h"
@@ -53,11 +55,42 @@
         return nullptr;
     }
 
+    auto eglGetError = reinterpret_cast<eglGetError_type>(
+            eglGetProcAddress("eglGetError"));
+    if (eglGetError == nullptr) {
+        ALOGE("Failed to load eglGetError");
+        return nullptr;
+    }
+
+    auto eglSurfaceAttrib = reinterpret_cast<eglSurfaceAttrib_type>(
+            eglGetProcAddress("eglSurfaceAttrib"));
+    if (eglSurfaceAttrib == nullptr) {
+        ALOGE("Failed to load eglSurfaceAttrib");
+        return nullptr;
+    }
+
+    // stats may not be supported on all versions
+    auto eglGetNextFrameIdANDROID = reinterpret_cast<eglGetNextFrameIdANDROID_type>(
+            eglGetProcAddress("eglGetNextFrameIdANDROID"));
+    if (eglGetNextFrameIdANDROID == nullptr) {
+        ALOGI("Failed to load eglGetNextFrameIdANDROID");
+    }
+
+    auto eglGetFrameTimestampsANDROID = reinterpret_cast<eglGetFrameTimestampsANDROID_type>(
+            eglGetProcAddress("eglGetFrameTimestampsANDROID"));
+    if (eglGetFrameTimestampsANDROID == nullptr) {
+        ALOGI("Failed to load eglGetFrameTimestampsANDROID");
+    }
+
     auto egl = std::make_unique<EGL>(refreshPeriod, ConstructorTag{});
     egl->eglPresentationTimeANDROID = eglPresentationTimeANDROID;
     egl->eglCreateSyncKHR = eglCreateSyncKHR;
     egl->eglDestroySyncKHR = eglDestroySyncKHR;
     egl->eglGetSyncAttribKHR = eglGetSyncAttribKHR;
+    egl->eglGetError = eglGetError;
+    egl->eglSurfaceAttrib = eglSurfaceAttrib;
+    egl->eglGetNextFrameIdANDROID = eglGetNextFrameIdANDROID;
+    egl->eglGetFrameTimestampsANDROID = eglGetFrameTimestampsANDROID;
     return egl;
 }
 
@@ -106,4 +139,68 @@
     return EGL_TRUE;
 }
 
+bool EGL::statsSupported() {
+    return (eglGetNextFrameIdANDROID != nullptr && eglGetFrameTimestampsANDROID != nullptr);
+}
+
+std::optional<EGLuint64KHR> EGL::getNextFrameId(EGLDisplay dpy, EGLSurface surface) {
+    if (eglGetNextFrameIdANDROID == nullptr) {
+        ALOGE("stats are not supported on this platform");
+        return std::nullopt;
+    }
+
+    EGLuint64KHR frameId;
+    EGLBoolean result = eglGetNextFrameIdANDROID(dpy, surface, &frameId);
+    if (result == EGL_FALSE) {
+        ALOGE("Failed to get next frame ID");
+        return std::nullopt;
+    }
+
+    return std::make_optional(frameId);
+}
+
+std::unique_ptr<EGL::FrameTimestamps> EGL::getFrameTimestamps(EGLDisplay dpy,
+                                                              EGLSurface surface,
+                                                              EGLuint64KHR frameId) {
+    if (eglGetFrameTimestampsANDROID == nullptr) {
+        ALOGE("stats are not supported on this platform");
+        return nullptr;
+    }
+
+    const std::vector<EGLint> timestamps = {
+            EGL_REQUESTED_PRESENT_TIME_ANDROID,
+            EGL_RENDERING_COMPLETE_TIME_ANDROID,
+            EGL_COMPOSITION_LATCH_TIME_ANDROID,
+            EGL_DISPLAY_PRESENT_TIME_ANDROID,
+    };
+
+    std::vector<EGLnsecsANDROID> values(timestamps.size());
+
+    EGLBoolean result = eglGetFrameTimestampsANDROID(dpy, surface, frameId,
+           timestamps.size(), timestamps.data(), values.data());
+    if (result == EGL_FALSE) {
+        EGLint reason = eglGetError();
+        if (reason == EGL_BAD_SURFACE) {
+            eglSurfaceAttrib(dpy, surface, EGL_TIMESTAMPS_ANDROID, EGL_TRUE);
+        } else {
+            ALOGE("Failed to get timestamps for frame %llu", (unsigned long long) frameId);
+        }
+        return nullptr;
+    }
+
+    // try again if we got some pending stats
+    for (auto i : values) {
+        if (i == EGL_TIMESTAMP_PENDING_ANDROID) return nullptr;
+    }
+
+    std::unique_ptr<EGL::FrameTimestamps> frameTimestamps =
+            std::make_unique<EGL::FrameTimestamps>();
+    frameTimestamps->requested = values[0];
+    frameTimestamps->renderingCompleted = values[1];
+    frameTimestamps->compositionLatched = values[2];
+    frameTimestamps->presented = values[3];
+
+    return frameTimestamps;
+}
+
 } // namespace swappy
diff --git a/src/swappy/src/main/cpp/EGL.h b/src/swappy/src/main/cpp/EGL.h
index 8da2292..c51db3e 100644
--- a/src/swappy/src/main/cpp/EGL.h
+++ b/src/swappy/src/main/cpp/EGL.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <mutex>
+#include <optional>
 
 #include <EGL/egl.h>
 #include <EGL/eglext.h>
@@ -31,6 +32,13 @@
     };
 
   public:
+    struct FrameTimestamps {
+        EGLnsecsANDROID requested;
+        EGLnsecsANDROID renderingCompleted;
+        EGLnsecsANDROID compositionLatched;
+        EGLnsecsANDROID presented;
+    };
+
     explicit EGL(std::chrono::nanoseconds refreshPeriod, ConstructorTag)
         : mRefreshPeriod(refreshPeriod) {}
 
@@ -42,6 +50,14 @@
                              EGLSurface surface,
                              std::chrono::steady_clock::time_point time);
 
+    // for stats
+    bool statsSupported();
+    std::optional<EGLuint64KHR> getNextFrameId(EGLDisplay dpy,
+                                               EGLSurface surface);
+    std::unique_ptr<FrameTimestamps> getFrameTimestamps(EGLDisplay dpy,
+                                                        EGLSurface surface,
+                                                        EGLuint64KHR frameId);
+
   private:
     const std::chrono::nanoseconds mRefreshPeriod;
 
@@ -54,6 +70,16 @@
     using eglGetSyncAttribKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR, EGLint, EGLint *);
     eglGetSyncAttribKHR_type eglGetSyncAttribKHR = nullptr;
 
+    using eglGetError_type = EGLint (*)(void);
+    eglGetError_type eglGetError = nullptr;
+    using eglSurfaceAttrib_type = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLint, EGLint);
+    eglSurfaceAttrib_type eglSurfaceAttrib = nullptr;
+    using eglGetNextFrameIdANDROID_type = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLuint64KHR *);
+    eglGetNextFrameIdANDROID_type eglGetNextFrameIdANDROID = nullptr;
+    using eglGetFrameTimestampsANDROID_type =  EGLBoolean (*)(EGLDisplay, EGLSurface,
+            EGLuint64KHR, EGLint, const EGLint *, EGLnsecsANDROID *);
+    eglGetFrameTimestampsANDROID_type eglGetFrameTimestampsANDROID = nullptr;
+
     std::mutex mSyncFenceMutex;
     EGLSyncKHR mSyncFence = EGL_NO_SYNC_KHR;
 };
diff --git a/src/swappy/src/main/cpp/FrameStatistics.cpp b/src/swappy/src/main/cpp/FrameStatistics.cpp
new file mode 100644
index 0000000..12b59c5
--- /dev/null
+++ b/src/swappy/src/main/cpp/FrameStatistics.cpp
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2019 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 "FrameStatistics.h"
+
+#define LOG_TAG "FrameStatistics"
+
+#include <cmath>
+#include <inttypes.h>
+#include <string>
+
+#include "EGL.h"
+
+#include "Log.h"
+
+
+namespace swappy {
+
+void FrameStatistics::updateFrames(EGLnsecsANDROID start, EGLnsecsANDROID end, uint64_t stat[]) {
+    const uint64_t deltaTimeNano = end - start;
+
+    uint32_t numFrames = deltaTimeNano / mRefreshPeriod.count();
+    numFrames = std::min(numFrames, static_cast<uint32_t>(MAX_FRAME_BUCKETS));
+    stat[numFrames]++;
+}
+
+void FrameStatistics::updateIdleFrames(EGL::FrameTimestamps& frameStats) {
+    updateFrames(frameStats.renderingCompleted,
+                 frameStats.compositionLatched,
+                 mStats.idleFrames);
+}
+
+void FrameStatistics::updateLatencyFrames(swappy::EGL::FrameTimestamps &frameStats,
+                                                                        TimePoint frameStartTime) {
+    updateFrames(frameStartTime.time_since_epoch().count(),
+                 frameStats.presented,
+                 mStats.latencyFrames);
+}
+
+void FrameStatistics::updateLateFrames(EGL::FrameTimestamps& frameStats) {
+    updateFrames(frameStats.requested,
+                 frameStats.presented,
+                 mStats.lateFrames);
+}
+
+void FrameStatistics::updateOffsetFromPreviousFrame(swappy::EGL::FrameTimestamps &frameStats) {
+    if (mPrevFrameTime != 0) {
+        updateFrames(mPrevFrameTime,
+                     frameStats.presented,
+                     mStats.offsetFromPreviousFrame);
+    }
+    mPrevFrameTime = frameStats.presented;
+}
+
+// called once per swap
+void FrameStatistics::capture(EGLDisplay dpy, EGLSurface surface) {
+    const TimePoint frameStartTime = std::chrono::steady_clock::now();
+
+    // first get the next frame id
+    std::optional<EGLuint64KHR> nextFrameId = mEgl->getNextFrameId(dpy, surface);
+    if (nextFrameId) {
+        mPendingFrames.push_back({dpy, surface, nextFrameId.value(), frameStartTime});
+    }
+
+    if (mPendingFrames.empty()) {
+        return;
+    }
+
+
+    EGLFrame frame = mPendingFrames.front();
+    // make sure we don't lag behind the stats too much
+    if (nextFrameId && nextFrameId.value() - frame.id > MAX_FRAME_LAG) {
+        while (mPendingFrames.size() > 1)
+            mPendingFrames.erase(mPendingFrames.begin());
+        mPrevFrameTime = 0;
+        frame = mPendingFrames.front();
+    }
+
+    std::unique_ptr<EGL::FrameTimestamps> frameStats =
+            mEgl->getFrameTimestamps(frame.dpy, frame.surface, frame.id);
+
+    if (!frameStats) {
+        return;
+    }
+
+    mPendingFrames.erase(mPendingFrames.begin());
+
+    std::lock_guard lock(mMutex);
+    mStats.totalFrames++;
+    updateIdleFrames(*frameStats);
+    updateLateFrames(*frameStats);
+    updateOffsetFromPreviousFrame(*frameStats);
+    updateLatencyFrames(*frameStats, frame.startFrameTime);
+
+    logFrames();
+}
+
+void FrameStatistics::logFrames() {
+    static auto previousLogTime = std::chrono::steady_clock::now();
+
+    if (std::chrono::steady_clock::now() - previousLogTime < LOG_EVERY_N_NS) {
+        return;
+    }
+
+    std::string message;
+    ALOGI("== Frame statistics ==");
+    ALOGI("total frames: %" PRIu64, mStats.totalFrames);
+    message += "Buckets:                    ";
+    for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
+        message += "\t[" + std::to_string(i) + "]";
+    ALOGI("%s", message.c_str());
+
+    message = "";
+    message += "idle frames:                ";
+    for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
+        message += "\t " + std::to_string(mStats.idleFrames[i]);
+    ALOGI("%s", message.c_str());
+
+    message = "";
+    message += "late frames:                ";
+    for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
+        message += "\t " + std::to_string(mStats.lateFrames[i]);
+    ALOGI("%s", message.c_str());
+
+    message = "";
+    message += "offset from previous frame: ";
+    for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
+        message += "\t " + std::to_string(mStats.offsetFromPreviousFrame[i]);
+    ALOGI("%s", message.c_str());
+
+    message = "";
+    message += "frame latency:              ";
+    for (int i = 0; i < MAX_FRAME_BUCKETS; i++)
+        message += "\t " + std::to_string(mStats.latencyFrames[i]);
+    ALOGI("%s", message.c_str());
+
+    previousLogTime = std::chrono::steady_clock::now();
+}
+
+Swappy_Stats FrameStatistics::getStats() {
+    std::lock_guard lock(mMutex);
+    return mStats;
+}
+
+} // namespace swappy
diff --git a/src/swappy/src/main/cpp/FrameStatistics.h b/src/swappy/src/main/cpp/FrameStatistics.h
new file mode 100644
index 0000000..fa8711b
--- /dev/null
+++ b/src/swappy/src/main/cpp/FrameStatistics.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2019 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 "EGL.h"
+#include "Thread.h"
+
+#include <array>
+#include <map>
+#include <vector>
+
+#include <swappy/swappy_extra.h>
+
+using TimePoint = std::chrono::steady_clock::time_point;
+using namespace std::chrono_literals;
+
+namespace swappy {
+
+
+class FrameStatistics {
+public:
+    explicit FrameStatistics(std::shared_ptr<EGL> egl,
+                             std::chrono::nanoseconds refreshPeriod) :
+            mEgl(egl), mRefreshPeriod(refreshPeriod) {};
+    ~FrameStatistics() = default;
+
+    void capture(EGLDisplay dpy, EGLSurface surface);
+
+    Swappy_Stats getStats();
+
+private:
+    static constexpr int MAX_FRAME_LAG = 10;
+    static constexpr std::chrono::nanoseconds LOG_EVERY_N_NS = 1s;
+
+    void updateFrames(EGLnsecsANDROID start, EGLnsecsANDROID end, uint64_t stat[]);
+    void updateIdleFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
+    void updateLateFrames(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
+    void updateOffsetFromPreviousFrame(EGL::FrameTimestamps& frameStats) REQUIRES(mMutex);
+    void updateLatencyFrames(EGL::FrameTimestamps& frameStats,
+                             TimePoint frameStartTime) REQUIRES(mMutex);
+    void logFrames() REQUIRES(mMutex);
+
+    std::shared_ptr<EGL> mEgl;
+    const std::chrono::nanoseconds mRefreshPeriod;
+
+    struct EGLFrame {
+        EGLDisplay dpy;
+        EGLSurface surface;
+        EGLuint64KHR id;
+        TimePoint startFrameTime;
+    };
+    std::vector<EGLFrame> mPendingFrames;
+    EGLnsecsANDROID mPrevFrameTime = 0;
+
+    std::mutex mMutex;
+    Swappy_Stats mStats GUARDED_BY(mMutex)= {};
+};
+
+} //namespace swappy
diff --git a/src/swappy/src/main/cpp/Swappy.cpp b/src/swappy/src/main/cpp/Swappy.cpp
index cb58ff0..1bfd1db 100644
--- a/src/swappy/src/main/cpp/Swappy.cpp
+++ b/src/swappy/src/main/cpp/Swappy.cpp
@@ -26,6 +26,7 @@
 #include "ChoreographerFilter.h"
 #include "ChoreographerThread.h"
 #include "EGL.h"
+#include "FrameStatistics.h"
 
 #include "Log.h"
 #include "Trace.h"
@@ -204,6 +205,57 @@
     swappy->mAutoSwapIntervalEnabled = enabled;
 }
 
+void Swappy::enableStats(bool enabled) {
+    Swappy *swappy = getInstance();
+    if (!swappy) {
+        ALOGE("Failed to get Swappy instance in enableStats");
+            return;
+    }
+
+    if (!swappy->getEgl()->statsSupported()) {
+        ALOGI("stats are not suppored on this platform");
+        return;
+    }
+
+    if (enabled && swappy->mFrameStatistics == nullptr) {
+        swappy->mFrameStatistics = std::make_unique<FrameStatistics>(
+                swappy->mEgl, swappy->mRefreshPeriod);
+        ALOGI("Enabling stats");
+    } else {
+        swappy->mFrameStatistics = nullptr;
+        ALOGI("Disabling stats");
+    }
+}
+
+void Swappy::recordFrameStart(EGLDisplay display, EGLSurface surface) {
+    TRACE_CALL();
+    Swappy *swappy = getInstance();
+    if (!swappy) {
+        ALOGE("Failed to get Swappy instance in recordFrameStart");
+        return;
+    }
+
+    if (swappy->mFrameStatistics) {
+        swappy->mFrameStatistics->capture(display, surface);
+    } else {
+        ALOGE("stats are not enabled");
+    }
+}
+
+void Swappy::getStats(Swappy_Stats *stats) {
+    Swappy *swappy = getInstance();
+    if (!swappy) {
+        ALOGE("Failed to get Swappy instance in getStats");
+        return;
+    }
+
+    if (swappy->mFrameStatistics) {
+        *stats = swappy->mFrameStatistics->getStats();
+    } else {
+        ALOGE("stats are not enabled");
+    }
+}
+
 Swappy *Swappy::getInstance() {
     std::lock_guard<std::mutex> lock(sInstanceMutex);
     return sInstance.get();
@@ -279,6 +331,7 @@
                nanoseconds sfOffset,
                ConstructorTag /*tag*/)
     : mRefreshPeriod(refreshPeriod),
+      mFrameStatistics(nullptr),
       mChoreographerFilter(std::make_unique<ChoreographerFilter>(refreshPeriod,
                                                                  sfOffset - appOffset,
                                                                  [this]() { return wakeClient(); })),
diff --git a/src/swappy/src/main/cpp/Swappy.h b/src/swappy/src/main/cpp/Swappy.h
index c4bec80..8544e83 100644
--- a/src/swappy/src/main/cpp/Swappy.h
+++ b/src/swappy/src/main/cpp/Swappy.h
@@ -37,6 +37,7 @@
 class ChoreographerFilter;
 class ChoreographerThread;
 class EGL;
+class FrameStatistics;
 
 using EGLDisplay = void *;
 using EGLSurface = void *;
@@ -74,6 +75,9 @@
 
     static void overrideAutoSwapInterval(uint64_t swap_ns);
 
+    static void enableStats(bool enabled);
+    static void recordFrameStart(EGLDisplay display, EGLSurface surface);
+    static void getStats(Swappy_Stats *stats);
     static void destroyInstance();
 
 private:
@@ -136,7 +140,7 @@
     int32_t mCurrentFrame = 0;
 
     std::mutex mEglMutex;
-    std::unique_ptr<EGL> mEgl;
+    std::shared_ptr<EGL> mEgl;
 
     int32_t mTargetFrame = 0;
     std::chrono::steady_clock::time_point mPresentationTime = std::chrono::steady_clock::now();
@@ -168,6 +172,7 @@
     bool mAutoSwapIntervalEnabled GUARDED_BY(mFrameDurationsMutex) = true;
     static constexpr float FRAME_AVERAGE_HYSTERESIS = 0.1;
     std::chrono::steady_clock::time_point mSwapTime;
+    std::unique_ptr<FrameStatistics> mFrameStatistics;
 };
 
 } //namespace swappy
diff --git a/src/swappy/src/main/cpp/Swappy_c.cpp b/src/swappy/src/main/cpp/Swappy_c.cpp
index b4eb810..158dca8 100644
--- a/src/swappy/src/main/cpp/Swappy_c.cpp
+++ b/src/swappy/src/main/cpp/Swappy_c.cpp
@@ -74,4 +74,16 @@
     Swappy::setAutoSwapInterval(enabled);
 }
 
+void Swappy_enableStats(bool enabled) {
+    Swappy::enableStats(enabled);
+}
+
+void Swappy_recordFrameStart(EGLDisplay display, EGLSurface surface) {
+    Swappy::recordFrameStart(display, surface);
+}
+
+void Swappy_getStats(Swappy_Stats *stats) {
+    Swappy::getStats(stats);
+}
+
 } // extern "C" {