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" {